Compare commits

...

22 Commits

Author SHA1 Message Date
Zlatin Balevsky
ff1df88601 Release 0.5.7 2019-11-03 12:35:04 +00:00
Zlatin Balevsky
4ed572ba51 clear search button 2019-11-03 12:03:12 +00:00
Zlatin Balevsky
fd3f55ab4d implement restore session 2019-11-03 10:06:55 +00:00
Zlatin Balevsky
1358e14467 add options for search history 2019-11-03 08:12:10 +00:00
Zlatin Balevsky
e22d5fea11 better search box 2019-11-03 01:50:55 +00:00
Zlatin Balevsky
7ade4aa10d set row height to trees 2019-11-02 19:06:26 +00:00
Zlatin Balevsky
a9f623a91a correct method name 2019-11-02 18:51:02 +00:00
Zlatin Balevsky
1ce410e943 wip on signing queries 2019-11-02 18:34:13 +00:00
Zlatin Balevsky
27aad9d75d do not collapse tree on updates pt2 2019-11-02 17:41:04 +00:00
Zlatin Balevsky
24591b10f2 change the griffon environment 2019-11-02 10:13:28 -07:00
Zlatin Balevsky
e4f1ea5c10 make table rows a bit larger 2019-11-02 15:58:48 +00:00
Zlatin Balevsky
c73c44c5f2 base table row height on the size of the font 2019-11-02 15:46:50 +00:00
Zlatin Balevsky
309cbcc580 UTF-8 in props of cli 2019-11-02 15:23:15 +00:00
Zlatin Balevsky
86894f242b support UTF-8 in persona names 2019-11-02 14:43:24 +00:00
Zlatin Balevsky
568255140f visualize the negative tree as well 2019-11-02 12:54:43 +00:00
Zlatin Balevsky
f6d2bac5bb show all watched directories 2019-11-02 12:26:19 +00:00
Zlatin Balevsky
1c396711ed Fix sidecar files larger than the limit from being shared 2019-11-02 11:15:08 +00:00
Zlatin Balevsky
c154d9538d only check negative tree for files, not directories 2019-11-02 10:28:04 +00:00
Zlatin Balevsky
8043782446 logging config with all logs turned off 2019-11-02 08:52:29 +00:00
Zlatin Balevsky
00c529cca1 toString() 2019-11-02 00:40:08 +00:00
Zlatin Balevsky
094b9ac2b0 restore behavior where watched directories get scanned on startup 2019-11-02 00:27:12 +00:00
Zlatin Balevsky
0dae0a561b more accurate speed measurement. Makes a difference if MW is minimized for a long time 2019-11-01 18:39:41 +00:00
38 changed files with 687 additions and 133 deletions

View File

@@ -32,7 +32,7 @@ import com.muwire.core.UILoadedEvent
import com.muwire.core.files.AllFilesLoadedEvent
class CliLanterna {
private static final String MW_VERSION = "0.5.6"
private static final String MW_VERSION = "0.5.7"
private static volatile Core core
@@ -82,14 +82,14 @@ class CliLanterna {
props.setDownloadLocation(downloadLocationFile)
props.incompleteLocation = incompletesLocationFile
propsFile.withOutputStream {
propsFile.withPrintWriter("UTF-8", {
props.write(it)
}
})
} else {
props = new Properties()
propsFile.withInputStream {
propsFile.withReader("UTF-8", {
props.load(it)
}
})
props = new MuWireSettings(props)
}
props.updateType = "cli-lanterna"

View File

@@ -8,7 +8,11 @@ import com.muwire.core.search.SearchEvent
import com.muwire.core.search.UIResultBatchEvent
import com.muwire.core.search.UIResultEvent
import net.i2p.crypto.DSAEngine
import net.i2p.data.Base64
import net.i2p.data.Signature
import java.nio.charset.StandardCharsets
import com.googlecode.lanterna.gui2.TextGUIThread
import com.googlecode.lanterna.gui2.table.TableModel
@@ -40,20 +44,27 @@ class SearchModel {
}
def searchEvent
byte [] payload
if (hashSearch) {
searchEvent = new SearchEvent(searchHash : root, uuid : UUID.randomUUID(), oobInfohash : true, compressedResults : true)
payload = root
} else {
def replaced = query.toLowerCase().trim().replaceAll(SplitPattern.SPLIT_PATTERN, " ")
def terms = replaced.split(" ")
def nonEmpty = []
terms.each { if (it.length() > 0) nonEmpty << it }
payload = String.join(" ", nonEmpty).getBytes(StandardCharsets.UTF_8)
searchEvent = new SearchEvent(searchTerms : nonEmpty, uuid : UUID.randomUUID(), oobInfohash: true,
searchComments : core.muOptions.searchComments, compressedResults : true)
}
boolean firstHop = core.muOptions.allowUntrusted || core.muOptions.searchExtraHop
Signature sig = DSAEngine.getInstance().sign(payload, core.spk)
core.eventBus.publish(new QueryEvent(searchEvent : searchEvent, firstHop : firstHop,
replyTo: core.me.destination, receivedOn: core.me.destination,
originator : core.me))
originator : core.me, sig: sig.data))
}
void unregister() {

View File

@@ -199,6 +199,6 @@ class TrustView extends BasicWindow {
private void saveMuSettings() {
File settingsFile = new File(core.home,"MuWire.properties")
settingsFile.withOutputStream { core.muOptions.write(it) }
settingsFile.withPrintWriter("UTF-8",{ core.muOptions.write(it) })
}
}

View File

@@ -103,6 +103,8 @@ public class Core {
private final Router router
final AtomicBoolean shutdown = new AtomicBoolean()
final SigningPrivateKey spk
public Core(MuWireSettings props, File home, String myVersion) {
this.home = home
@@ -180,7 +182,7 @@ public class Core {
i2pSession = socketManager.getSession()
def destination = new Destination()
def spk = new SigningPrivateKey(Constants.SIG_TYPE)
spk = new SigningPrivateKey(Constants.SIG_TYPE)
keyDat.withInputStream {
destination.readBytes(it)
def privateKey = new PrivateKey()
@@ -191,8 +193,9 @@ public class Core {
def baos = new ByteArrayOutputStream()
def daos = new DataOutputStream(baos)
daos.write(Constants.PERSONA_VERSION)
daos.writeShort((short)props.getNickname().length())
daos.write(props.getNickname().getBytes(StandardCharsets.UTF_8))
byte [] name = props.getNickname().getBytes(StandardCharsets.UTF_8)
daos.writeShort((short)name.length)
daos.write(name)
destination.writeBytes(daos)
daos.flush()
byte [] payload = baos.toByteArray()
@@ -335,8 +338,7 @@ public class Core {
return
}
log.info("saving settings")
File f = new File(home, "MuWire.properties")
f.withOutputStream { muOptions.write(it) }
saveMuSettings()
log.info("shutting down trust subscriber")
trustSubscriber.stop()
log.info("shutting down download manageer")
@@ -357,6 +359,11 @@ public class Core {
}
log.info("shutdown complete")
}
public void saveMuSettings() {
File f = new File(home, "MuWire.properties")
f.withPrintWriter("UTF-8", { muOptions.write(it) })
}
static main(args) {
def home = System.getProperty("user.home") + File.separator + ".MuWire"
@@ -382,7 +389,7 @@ public class Core {
}
}
Core core = new Core(props, home, "0.5.6")
Core core = new Core(props, home, "0.5.7")
core.startServices()
// ... at the end, sleep or execute script

View File

@@ -78,10 +78,10 @@ class MuWireSettings {
totalUploadSlots = Integer.valueOf(props.getProperty("totalUploadSlots","-1"))
uploadSlotsPerUser = Integer.valueOf(props.getProperty("uploadSlotsPerUser","-1"))
watchedDirectories = readEncodedSet(props, "watchedDirectories")
watchedKeywords = readEncodedSet(props, "watchedKeywords")
watchedRegexes = readEncodedSet(props, "watchedRegexes")
negativeFileTree = readEncodedSet(props, "negativeFileTree")
watchedDirectories = DataUtil.readEncodedSet(props, "watchedDirectories")
watchedKeywords = DataUtil.readEncodedSet(props, "watchedKeywords")
watchedRegexes = DataUtil.readEncodedSet(props, "watchedRegexes")
negativeFileTree = DataUtil.readEncodedSet(props, "negativeFileTree")
trustSubscriptions = new HashSet<>()
if (props.containsKey("trustSubscriptions")) {
@@ -93,7 +93,7 @@ class MuWireSettings {
}
void write(OutputStream out) throws IOException {
void write(Writer out) throws IOException {
Properties props = new Properties()
props.setProperty("leaf", isLeaf.toString())
props.setProperty("allowUntrusted", allowUntrusted.toString())
@@ -125,10 +125,10 @@ class MuWireSettings {
props.setProperty("totalUploadSlots", String.valueOf(totalUploadSlots))
props.setProperty("uploadSlotsPerUser", String.valueOf(uploadSlotsPerUser))
writeEncodedSet(watchedDirectories, "watchedDirectories", props)
writeEncodedSet(watchedKeywords, "watchedKeywords", props)
writeEncodedSet(watchedRegexes, "watchedRegexes", props)
writeEncodedSet(negativeFileTree, "negativeFileTree", props)
DataUtil.writeEncodedSet(watchedDirectories, "watchedDirectories", props)
DataUtil.writeEncodedSet(watchedKeywords, "watchedKeywords", props)
DataUtil.writeEncodedSet(watchedRegexes, "watchedRegexes", props)
DataUtil.writeEncodedSet(negativeFileTree, "negativeFileTree", props)
if (!trustSubscriptions.isEmpty()) {
String encoded = trustSubscriptions.stream().
@@ -137,25 +137,7 @@ class MuWireSettings {
props.setProperty("trustSubscriptions", encoded)
}
props.store(out, "")
}
private static Set<String> readEncodedSet(Properties props, String property) {
Set<String> rv = new ConcurrentHashSet<>()
if (props.containsKey(property)) {
String[] encoded = props.getProperty(property).split(",")
encoded.each { rv << DataUtil.readi18nString(Base64.decode(it)) }
}
rv
}
private static void writeEncodedSet(Set<String> set, String property, Properties props) {
if (set.isEmpty())
return
String encoded = set.stream().
map({Base64.encode(DataUtil.encodei18nString(it))}).
collect(Collectors.joining(","))
props.setProperty(property, encoded)
props.store(out, "This file is UTF-8")
}
boolean isLeaf() {

View File

@@ -22,8 +22,9 @@ public class Name {
public void write(OutputStream out) throws IOException {
DataOutputStream dos = new DataOutputStream(out)
dos.writeShort(name.length())
dos.write(name.getBytes(StandardCharsets.UTF_8))
byte [] bytes = name.getBytes(StandardCharsets.UTF_8)
dos.writeShort(bytes.length)
dos.write(bytes)
}
public getName() {

View File

@@ -1,5 +1,6 @@
package com.muwire.core.connection
import java.nio.charset.StandardCharsets
import java.util.concurrent.BlockingQueue
import java.util.concurrent.CountDownLatch
import java.util.concurrent.ExecutorService
@@ -10,6 +11,7 @@ import java.util.concurrent.TimeUnit
import java.util.concurrent.atomic.AtomicBoolean
import java.util.logging.Level
import com.muwire.core.Constants
import com.muwire.core.EventBus
import com.muwire.core.MuWireSettings
import com.muwire.core.Persona
@@ -21,8 +23,10 @@ import com.muwire.core.trust.TrustLevel
import com.muwire.core.trust.TrustService
import groovy.util.logging.Log
import net.i2p.crypto.DSAEngine
import net.i2p.data.Base64
import net.i2p.data.Destination
import net.i2p.data.Signature
@Log
abstract class Connection implements Closeable {
@@ -147,6 +151,8 @@ abstract class Connection implements Closeable {
query.replyTo = e.replyTo.toBase64()
if (e.originator != null)
query.originator = e.originator.toBase64()
if (e.sig != null)
query.sig = Base64.encode(e.sig)
messages.put(query)
}
@@ -225,6 +231,24 @@ abstract class Connection implements Closeable {
boolean compressedResults = false
if (search.compressedResults != null)
compressedResults = search.compressedResults
byte[] sig = null
// TODO: make this mandatory at some point
if (search.sig != null) {
sig = Base64.decode(search.sig)
byte [] payload
if (infohash != null)
payload = infohash
else
payload = String.join(" ",search.keywords).getBytes(StandardCharsets.UTF_8)
def spk = originator.destination.getSigningPublicKey()
def signature = new Signature(Constants.SIG_TYPE, sig)
if (!DSAEngine.getInstance().verifySignature(signature, payload, spk)) {
log.info("signature didn't match keywords")
return
} else
log.info("query signature verified")
} else
log.info("no signature in query")
SearchEvent searchEvent = new SearchEvent(searchTerms : search.keywords,
searchHash : infohash,
@@ -236,7 +260,8 @@ abstract class Connection implements Closeable {
replyTo : replyTo,
originator : originator,
receivedOn : endpoint.destination,
firstHop : search.firstHop )
firstHop : search.firstHop,
sig : sig )
eventBus.publish(event)
}

View File

@@ -21,6 +21,7 @@ import java.nio.file.Files
import java.nio.file.StandardOpenOption
import java.security.MessageDigest
import java.security.NoSuchAlgorithmException
import java.util.concurrent.atomic.AtomicLong
import java.util.logging.Level
@Log
@@ -37,13 +38,12 @@ class DownloadSession {
private final Set<Integer> available
private final MessageDigest digest
private long lastSpeedRead = System.currentTimeMillis()
private long dataSinceLastRead
private final AtomicLong dataSinceLastRead
private MappedByteBuffer mapped
DownloadSession(EventBus eventBus, String meB64, Pieces pieces, InfoHash infoHash, Endpoint endpoint, File file,
int pieceSize, long fileLength, Set<Integer> available) {
int pieceSize, long fileLength, Set<Integer> available, AtomicLong dataSinceLastRead) {
this.eventBus = eventBus
this.meB64 = meB64
this.pieces = pieces
@@ -53,6 +53,7 @@ class DownloadSession {
this.pieceSize = pieceSize
this.fileLength = fileLength
this.available = available
this.dataSinceLastRead = dataSinceLastRead
try {
digest = MessageDigest.getInstance("SHA-256")
} catch (NoSuchAlgorithmException impossible) {
@@ -190,7 +191,7 @@ class DownloadSession {
throw new IOException()
synchronized(this) {
mapped.put(tmp, 0, read)
dataSinceLastRead += read
dataSinceLastRead.addAndGet(read)
pieces.markPartial(piece, mapped.position())
}
}
@@ -222,13 +223,4 @@ class DownloadSession {
return 0
mapped.position()
}
synchronized int speed() {
final long now = System.currentTimeMillis()
long interval = Math.max(1000, now - lastSpeedRead)
lastSpeedRead = now;
int rv = (int) (dataSinceLastRead * 1000.0 / interval)
dataSinceLastRead = 0
rv
}
}

View File

@@ -12,6 +12,7 @@ import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.ExecutorService
import java.util.concurrent.Executors
import java.util.concurrent.atomic.AtomicBoolean
import java.util.concurrent.atomic.AtomicLong
import java.util.logging.Level
import com.muwire.core.Constants
@@ -61,10 +62,11 @@ public class Downloader {
private final AtomicBoolean eventFired = new AtomicBoolean()
private boolean piecesFileClosed
private final AtomicLong dataSinceLastRead = new AtomicLong(0)
private volatile long lastSpeedRead = System.currentTimeMillis()
private ArrayList speedArr = new ArrayList<Integer>()
private int speedPos = 0
private int speedAvg = 0
private long timestamp = Instant.now().toEpochMilli()
public Downloader(EventBus eventBus, DownloadManager downloadManager,
Persona me, File file, long length, InfoHash infoHash,
@@ -139,10 +141,11 @@ public class Downloader {
public int speed() {
int currSpeed = 0
if (getCurrentState() == DownloadState.DOWNLOADING) {
activeWorkers.values().each {
if (it.currentState == WorkerState.DOWNLOADING)
currSpeed += it.speed()
}
long dataRead = dataSinceLastRead.getAndSet(0)
long now = System.currentTimeMillis()
if (now > lastSpeedRead)
currSpeed = (int) (dataRead * 1000.0 / (now - lastSpeedRead))
lastSpeedRead = now
}
if (speedArr.size() != downloadManager.muSettings.speedSmoothSeconds) {
@@ -302,7 +305,7 @@ public class Downloader {
boolean requestPerformed
while(!pieces.isComplete()) {
currentSession = new DownloadSession(eventBus, me.toBase64(), pieces, getInfoHash(),
endpoint, incompleteFile, pieceSize, length, available)
endpoint, incompleteFile, pieceSize, length, available, dataSinceLastRead)
requestPerformed = currentSession.request()
if (!requestPerformed)
break
@@ -339,12 +342,6 @@ public class Downloader {
}
}
int speed() {
if (currentSession == null)
return 0
currentSession.speed()
}
void cancel() {
downloadThread?.interrupt()
}

View File

@@ -86,9 +86,9 @@ class DirectoryWatcher {
private void saveMuSettings() {
File muSettingsFile = new File(home, "MuWire.properties")
muSettingsFile.withOutputStream {
muSettingsFile.withPrintWriter("UTF-8", {
muOptions.write(it)
}
})
}
private void watch() {

View File

@@ -45,7 +45,7 @@ class FileTree {
true
}
private static class TreeNode {
public static class TreeNode {
TreeNode parent
File file
final Set<TreeNode> children = new HashSet<>()

View File

@@ -34,9 +34,12 @@ class HasherService {
return
if (fileManager.fileToSharedFile.containsKey(canonical))
return
if (canonical.getName().endsWith(".mwcomment") && canonical.length() <= Constants.MAX_COMMENT_LENGTH)
eventBus.publish(new SideCarFileEvent(file : canonical))
else if (hashed.add(canonical))
if (canonical.isFile() && fileManager.negativeTree.fileToNode.containsKey(canonical))
return
if (canonical.getName().endsWith(".mwcomment")) {
if (canonical.length() <= Constants.MAX_COMMENT_LENGTH)
eventBus.publish(new SideCarFileEvent(file : canonical))
} else if (hashed.add(canonical))
executor.execute( { -> process(canonical) } as Runnable)
}

View File

@@ -4,4 +4,9 @@ import com.muwire.core.Event
class SideCarFileEvent extends Event {
File file
@Override
public String toString() {
return super.toString() + " file: "+file.getAbsolutePath()
}
}

View File

@@ -12,6 +12,7 @@ class QueryEvent extends Event {
Destination replyTo
Persona originator
Destination receivedOn
byte[] sig
String toString() {
"searchEvent: $searchEvent firstHop:$firstHop, replyTo:${replyTo.toBase32()}" +

View File

@@ -11,10 +11,14 @@ import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import java.util.Properties;
import java.util.Set;
import java.util.stream.Collectors;
import com.muwire.core.Constants;
import net.i2p.data.Base64;
import net.i2p.util.ConcurrentHashSet;
public class DataUtil {
@@ -165,4 +169,22 @@ public class DataUtil {
} catch(Exception ex) { }
cb = null;
}
public static Set<String> readEncodedSet(Properties props, String property) {
Set<String> rv = new ConcurrentHashSet<>();
if (props.containsKey(property)) {
String [] encoded = props.getProperty(property).split(",");
for(String s : encoded)
rv.add(readi18nString(Base64.decode(s)));
}
return rv;
}
public static void writeEncodedSet(Set<String> set, String property, Properties props) {
if (set.isEmpty())
return;
String encoded = set.stream().map(s -> Base64.encode(encodei18nString(s)))
.collect(Collectors.joining(","));
props.setProperty(property, encoded);
}
}

View File

@@ -2,6 +2,8 @@ package com.muwire.core.download
import static org.junit.Assert.fail
import java.util.concurrent.atomic.AtomicLong
import org.junit.After
import org.junit.Before
import org.junit.Ignore
@@ -76,7 +78,7 @@ class DownloadSessionTest {
toUploader = new PipedOutputStream(fromDownloader)
endpoint = new Endpoint(null, fromUploader, toUploader, null)
session = new DownloadSession(eventBus, "",pieces, infoHash, endpoint, target, pieceSize, size, available)
session = new DownloadSession(eventBus, "",pieces, infoHash, endpoint, target, pieceSize, size, available, new AtomicLong())
downloadThread = new Thread( { perform() } as Runnable)
downloadThread.setDaemon(true)
downloadThread.start()

View File

@@ -1,11 +1,12 @@
group = com.muwire
version = 0.5.6
version = 0.5.7
i2pVersion = 0.9.43
groovyVersion = 2.4.15
slf4jVersion = 1.7.25
spockVersion = 1.1-groovy-2.4
grailsVersion=4.0.0
gorm.version=7.0.2.RELEASE
griffonEnv=prod
sourceCompatibility=1.8
targetCompatibility=1.8

View File

@@ -81,4 +81,9 @@ mvcGroups {
view = 'com.muwire.gui.UpdateView'
controller = 'com.muwire.gui.UpdateController'
}
'advanced-sharing' {
model = 'com.muwire.gui.AdvancedSharingModel'
view = 'com.muwire.gui.AdvancedSharingView'
controller = 'com.muwire.gui.AdvancedSharingController'
}
}

View File

@@ -0,0 +1,17 @@
package com.muwire.gui
import griffon.core.artifact.GriffonController
import griffon.core.controller.ControllerAction
import griffon.inject.MVCMember
import griffon.metadata.ArtifactProviderFor
import javax.annotation.Nonnull
import com.muwire.core.Core
@ArtifactProviderFor(GriffonController)
class AdvancedSharingController {
@MVCMember @Nonnull
AdvancedSharingModel model
@MVCMember @Nonnull
AdvancedSharingView view
}

View File

@@ -97,9 +97,6 @@ class ContentPanelController {
}
void saveMuWireSettings() {
File f = new File(core.home, "MuWire.properties")
f.withOutputStream {
core.muOptions.write(it)
}
core.saveMuSettings()
}
}

View File

@@ -7,7 +7,12 @@ import griffon.core.mvc.MVCGroup
import griffon.core.mvc.MVCGroupConfiguration
import griffon.inject.MVCMember
import griffon.metadata.ArtifactProviderFor
import net.i2p.crypto.DSAEngine
import net.i2p.data.Base64
import net.i2p.data.Signature
import java.awt.event.ActionEvent
import java.nio.charset.StandardCharsets
import javax.annotation.Nonnull
import javax.inject.Inject
@@ -47,11 +52,29 @@ class MainFrameController {
private volatile Core core
@ControllerAction
void search() {
void clearSearch() {
def searchField = builder.getVariable("search-field")
searchField.setSelectedItem(null)
searchField.requestFocus()
}
@ControllerAction
void search(ActionEvent evt) {
if (evt?.getActionCommand() == null)
return
def cardsPanel = builder.getVariable("cards-panel")
cardsPanel.getLayout().show(cardsPanel, "search window")
def search = builder.getVariable("search-field").text
def searchField = builder.getVariable("search-field")
def search = searchField.getSelectedItem()
searchField.model.addElement(search)
performSearch(search)
}
private void performSearch(String search) {
model.sessionRestored = true
search = search.trim()
if (search.length() == 0)
return
@@ -77,21 +100,28 @@ class MainFrameController {
}
def searchEvent
byte [] payload
if (hashSearch) {
searchEvent = new SearchEvent(searchHash : root, uuid : uuid, oobInfohash: true, compressedResults : true)
payload = root
} else {
// this can be improved a lot
def replaced = search.toLowerCase().trim().replaceAll(SplitPattern.SPLIT_PATTERN, " ")
def terms = replaced.split(" ")
def nonEmpty = []
terms.each { if (it.length() > 0) nonEmpty << it }
searchEvent = new SearchEvent(searchTerms : nonEmpty, uuid : uuid, oobInfohash: true,
searchComments : core.muOptions.searchComments, compressedResults : true)
payload = String.join(" ",nonEmpty).getBytes(StandardCharsets.UTF_8)
searchEvent = new SearchEvent(searchTerms : nonEmpty, uuid : uuid, oobInfohash: true,
searchComments : core.muOptions.searchComments, compressedResults : true)
}
boolean firstHop = core.muOptions.allowUntrusted || core.muOptions.searchExtraHop
Signature sig = DSAEngine.getInstance().sign(payload, core.spk)
core.eventBus.publish(new QueryEvent(searchEvent : searchEvent, firstHop : firstHop,
replyTo: core.me.destination, receivedOn: core.me.destination,
originator : core.me))
replyTo: core.me.destination, receivedOn: core.me.destination,
originator : core.me, sig : sig.data))
}
void search(String infoHash, String tabTitle) {
@@ -297,12 +327,17 @@ class MainFrameController {
void clearUploads() {
model.uploads.removeAll { it.finished }
}
@ControllerAction
void restoreSession() {
model.sessionRestored = true
view.settings.openTabs.each {
performSearch(it)
}
}
void saveMuWireSettings() {
File f = new File(core.home, "MuWire.properties")
f.withOutputStream {
core.muOptions.write(it)
}
core.saveMuSettings()
}
void mvcGroupInit(Map<String, String> args) {

View File

@@ -10,6 +10,7 @@ import java.util.logging.Level
import javax.annotation.Nonnull
import javax.swing.JFileChooser
import javax.swing.JOptionPane
import com.muwire.core.Core
import com.muwire.core.MuWireSettings
@@ -20,12 +21,14 @@ class OptionsController {
OptionsModel model
@MVCMember @Nonnull
OptionsView view
Core core
MuWireSettings settings
UISettings uiSettings
@ControllerAction
void save() {
String text
Core core = application.context.get("core")
MuWireSettings settings = application.context.get("muwire-settings")
def i2pProps = core.i2pOptions
@@ -137,14 +140,10 @@ class OptionsController {
model.trustListInterval = trustListInterval
settings.trustListInterval = Integer.parseInt(trustListInterval)
File settingsFile = new File(core.home, "MuWire.properties")
settingsFile.withOutputStream {
settings.write(it)
}
core.saveMuSettings()
// UI Setttings
UISettings uiSettings = application.context.get("ui-settings")
text = view.lnfField.text
model.lnf = text
uiSettings.lnf = text
@@ -172,16 +171,24 @@ class OptionsController {
model.clearUploads = clearUploads
uiSettings.clearUploads = clearUploads
boolean storeSearchHistory = view.storeSearchHistoryCheckbox.model.isSelected()
model.storeSearchHistory = storeSearchHistory
uiSettings.storeSearchHistory = storeSearchHistory
uiSettings.exitOnClose = model.exitOnClose
if (model.closeDecisionMade)
uiSettings.closeWarning = false
saveUISettings()
cancel()
}
private void saveUISettings() {
File uiSettingsFile = new File(core.home, "gui.properties")
uiSettingsFile.withOutputStream {
uiSettings.write(it)
}
cancel()
}
@ControllerAction
@@ -234,4 +241,11 @@ class OptionsController {
model.exitOnClose = false
model.closeDecisionMade = true
}
@ControllerAction
void clearHistory() {
uiSettings.searchHistory.clear()
saveUISettings()
JOptionPane.showMessageDialog(null, "Search history has been cleared")
}
}

View File

@@ -117,6 +117,7 @@ class Initialize extends AbstractLifecycleHandler {
def guiPropsFile = new File(home, "gui.properties")
UISettings uiSettings
int rowHeight = 15
if (guiPropsFile.exists()) {
Properties props = new Properties()
guiPropsFile.withInputStream { props.load(it) }
@@ -149,7 +150,7 @@ class Initialize extends AbstractLifecycleHandler {
} else {
fontSize = uiSettings.fontSize
}
rowHeight = fontSize + 3
FontUIResource font = new FontUIResource(fontName, Font.PLAIN, fontSize)
def keys = lnf.getDefaults().keys()
@@ -183,6 +184,7 @@ class Initialize extends AbstractLifecycleHandler {
}
}
application.context.put("row-height", rowHeight)
application.context.put("ui-settings", uiSettings)
}

View File

@@ -41,9 +41,9 @@ class Ready extends AbstractLifecycleHandler {
def propsFile = new File(home, "MuWire.properties")
if (propsFile.exists()) {
log.info("loading existing props file")
propsFile.withInputStream {
propsFile.withReader("UTF-8", {
props.load(it)
}
})
props = new MuWireSettings(props)
if (props.incompleteLocation == null)
props.incompleteLocation = new File(home, "incompletes")
@@ -90,9 +90,9 @@ class Ready extends AbstractLifecycleHandler {
props.downloadLocation = chooser.getSelectedFile()
}
propsFile.withOutputStream {
propsFile.withPrintWriter("UTF-8", {
props.write(it)
}
})
}
Core core

View File

@@ -0,0 +1,39 @@
package com.muwire.gui
import javax.swing.tree.DefaultMutableTreeNode
import javax.swing.tree.DefaultTreeModel
import javax.swing.tree.MutableTreeNode
import com.muwire.core.Core
import com.muwire.core.files.FileTree
import griffon.core.artifact.GriffonModel
import griffon.transform.Observable
import griffon.metadata.ArtifactProviderFor
@ArtifactProviderFor(GriffonModel)
class AdvancedSharingModel {
def watchedDirectories = []
def treeRoot
def negativeTree
Core core
void mvcGroupInit(Map<String,String> args) {
watchedDirectories.addAll(core.muOptions.watchedDirectories)
treeRoot = new DefaultMutableTreeNode()
negativeTree = new DefaultTreeModel(treeRoot)
copyTree(treeRoot, core.fileManager.negativeTree.root)
}
private void copyTree(DefaultMutableTreeNode jtreeNode, FileTree.TreeNode fileTreeNode) {
jtreeNode.setUserObject(fileTreeNode.file?.getName())
fileTreeNode.children.each {
MutableTreeNode newChild = new DefaultMutableTreeNode()
jtreeNode.add(newChild)
copyTree(newChild, it)
}
}
}

View File

@@ -88,6 +88,8 @@ class MainFrameModel {
def trusted = []
def distrusted = []
def subscriptions = []
boolean sessionRestored
@Observable int connections
@Observable String me
@@ -255,7 +257,7 @@ class MainFrameModel {
void onAllFilesLoadedEvent(AllFilesLoadedEvent e) {
runInsideUIAsync {
core.muOptions.watchedDirectories.each { core.eventBus.publish(new DirectoryWatchedEvent(directory : new File(it))) }
core.muOptions.watchedDirectories.each { core.eventBus.publish(new FileSharedEvent(file : new File(it))) }
core.muOptions.trustSubscriptions.each {
core.eventBus.publish(new TrustSubscriptionEvent(persona : it, subscribe : true))
@@ -295,6 +297,8 @@ class MainFrameModel {
runInsideUIAsync {
connections = core.connectionManager.getConnections().size()
view.showRestoreOrEmpty()
if (connections > 0) {
def topPanel = builder.getVariable("top-panel")
topPanel.getLayout().show(topPanel, "top-search-panel")

View File

@@ -21,6 +21,7 @@ class OptionsModel {
@Observable int speedSmoothSeconds
@Observable int totalUploadSlots
@Observable int uploadSlotsPerUser
@Observable boolean storeSearchHistory
// i2p options
@Observable String inboundLength
@@ -90,6 +91,7 @@ class OptionsModel {
showSearchHashes = uiSettings.showSearchHashes
clearUploads = uiSettings.clearUploads
exitOnClose = uiSettings.exitOnClose
storeSearchHistory = uiSettings.storeSearchHistory
if (core.router != null) {
inBw = String.valueOf(settings.inBw)

View File

@@ -0,0 +1,82 @@
package com.muwire.gui
import griffon.core.artifact.GriffonView
import griffon.inject.MVCMember
import griffon.metadata.ArtifactProviderFor
import javax.swing.JDialog
import javax.swing.JTabbedPane
import javax.swing.JTree
import javax.swing.SwingConstants
import java.awt.BorderLayout
import java.awt.event.WindowAdapter
import java.awt.event.WindowEvent
import javax.annotation.Nonnull
@ArtifactProviderFor(GriffonView)
class AdvancedSharingView {
@MVCMember @Nonnull
FactoryBuilderSupport builder
@MVCMember @Nonnull
AdvancedSharingModel model
def mainFrame
def dialog
def watchedDirsPanel
def negativeTreePanel
def watchedDirsTable
void initUI() {
mainFrame = application.windowManager.findWindow("main-frame")
int rowHeight = application.context.get("row-height")
dialog = new JDialog(mainFrame,"Advanced Sharing",true)
dialog.setResizable(true)
watchedDirsPanel = builder.panel {
borderLayout()
panel (constraints : BorderLayout.NORTH) {
label(text : "Directories watched for file changes")
}
scrollPane( constraints : BorderLayout.CENTER ) {
watchedDirsTable = table(autoCreateRowSorter : true, rowHeight : rowHeight) {
tableModel(list : model.watchedDirectories) {
closureColumn(header : "Directory", type : String, read : {it})
}
}
}
}
negativeTreePanel = builder.panel {
borderLayout()
panel(constraints : BorderLayout.NORTH) {
label(text : "Files which are explicitly not shared")
}
scrollPane( constraints : BorderLayout.CENTER ) {
def jtree = new JTree(model.negativeTree)
tree(rootVisible : false, rowHeight : rowHeight,jtree)
}
}
}
void mvcGroupInit(Map<String,String> args) {
def tabbedPane = new JTabbedPane()
tabbedPane.addTab("Watched Directories", watchedDirsPanel)
tabbedPane.addTab("Negative Tree", negativeTreePanel)
dialog.with {
getContentPane().add(tabbedPane)
pack()
setLocationRelativeTo(mainFrame)
setDefaultCloseOperation(JDialog.DISPOSE_ON_CLOSE)
addWindowListener(new WindowAdapter() {
public void windowClosed(WindowEvent e) {
mvcGroup.destroy()
}
})
show()
}
}
}

View File

@@ -38,6 +38,7 @@ class ContentPanelView {
void initUI() {
mainFrame = application.windowManager.findWindow("main-frame")
int rowHeight = application.context.get("row-height")
dialog = new JDialog(mainFrame, "Content Control Panel", true)
mainPanel = builder.panel {
@@ -48,7 +49,7 @@ class ContentPanelView {
label(text : "Rules")
}
scrollPane (constraints : BorderLayout.CENTER) {
rulesTable = table(id : "rules-table", autoCreateRowSorter : true) {
rulesTable = table(id : "rules-table", autoCreateRowSorter : true, rowHeight : rowHeight) {
tableModel(list : model.rules) {
closureColumn(header: "Term", type:String, read: {row -> row.getTerm()})
closureColumn(header: "Regex?", type:Boolean, read: {row -> row instanceof RegexMatcher})
@@ -74,7 +75,7 @@ class ContentPanelView {
label(text : "Hits")
}
scrollPane(constraints : BorderLayout.CENTER) {
hitsTable = table(id : "hits-table", autoCreateRowSorter : true) {
hitsTable = table(id : "hits-table", autoCreateRowSorter : true, rowHeight : rowHeight) {
tableModel(list : model.hits) {
closureColumn(header : "Searcher", type : String, read : {row -> row.persona.getHumanReadableName()})
closureColumn(header : "Keywords", type : String, read : {row -> row.keywords.join(" ")})

View File

@@ -11,6 +11,7 @@ import net.i2p.data.DataHelper
import javax.swing.BorderFactory
import javax.swing.Box
import javax.swing.BoxLayout
import javax.swing.JComboBox
import javax.swing.JFileChooser
import javax.swing.JFrame
import javax.swing.JLabel
@@ -24,6 +25,10 @@ import javax.swing.SwingConstants
import javax.swing.SwingUtilities
import javax.swing.TransferHandler
import javax.swing.border.Border
import javax.swing.event.DocumentEvent
import javax.swing.event.DocumentListener
import javax.swing.event.TreeExpansionEvent
import javax.swing.event.TreeExpansionListener
import javax.swing.table.DefaultTableCellRenderer
import javax.swing.tree.TreeNode
import javax.swing.tree.TreePath
@@ -67,11 +72,14 @@ class MainFrameView {
def lastDownloadSortEvent
def lastSharedSortEvent
def trustTablesSortEvents = [:]
def expansionListener = new TreeExpansions()
UISettings settings
void initUI() {
settings = application.context.get("ui-settings")
int rowHeight = application.context.get("row-height")
builder.with {
application(size : [1024,768], id: 'main-frame',
locationRelativeTo : null,
@@ -89,11 +97,12 @@ class MainFrameView {
menuItem("Exit", actionPerformed : {closeApplication()})
}
menu (text : "Options") {
menuItem("Configuration", actionPerformed : {mvcGroup.createMVCGroup("Options")})
menuItem("Content Control", actionPerformed : {
def env = [:]
env["core"] = model.core
mvcGroup.createMVCGroup("content-panel", env)
menuItem("Configuration", actionPerformed : {
def params = [:]
params['core'] = application.context.get("core")
params['settings'] = params['core'].muOptions
params['uiSettings'] = settings
mvcGroup.createMVCGroup("Options", params)
})
}
menu (text : "Status") {
@@ -102,6 +111,18 @@ class MainFrameView {
menuItem("I2P", enabled : bind {model.routerPresent}, actionPerformed: {mvcGroup.createMVCGroup("i-2-p-status")})
menuItem("System", actionPerformed : {mvcGroup.createMVCGroup("system-status")})
}
menu (text : "Tools") {
menuItem("Content Control", actionPerformed : {
def env = [:]
env["core"] = model.core
mvcGroup.createMVCGroup("content-panel", env)
})
menuItem("Advanced Sharing", actionPerformed : {
def env = [:]
env["core"] = model.core
mvcGroup.createMVCGroup("advanced-sharing",env)
})
}
}
borderLayout()
panel (border: etchedBorder(), constraints : BorderLayout.NORTH) {
@@ -124,20 +145,39 @@ class MainFrameView {
panel(constraints: BorderLayout.CENTER) {
borderLayout()
label(" Enter search here:", constraints: BorderLayout.WEST) // TODO: fix this
textField(id: "search-field", constraints: BorderLayout.CENTER, action : searchAction)
def searchFieldModel = new SearchFieldModel(settings, new File(application.context.get("muwire-home")))
JComboBox myComboBox = new SearchField(searchFieldModel)
myComboBox.setAction(searchAction)
widget(id: "search-field", constraints: BorderLayout.CENTER, myComboBox)
}
panel( constraints: BorderLayout.EAST) {
button(text: "Search", searchAction)
button(text : "", icon : imageIcon("/close_tab.png"), clearSearchAction)
}
}
}
}
panel (id: "cards-panel", constraints : BorderLayout.CENTER) {
cardLayout()
panel (constraints : "search window") {
borderLayout()
tabbedPane(id : "result-tabs", constraints: BorderLayout.CENTER)
panel (id : "search window", constraints : "search window") {
cardLayout()
panel (constraints : "tabs-panel") {
borderLayout()
tabbedPane(id : "result-tabs", constraints: BorderLayout.CENTER)
}
panel(constraints : "restore session") {
borderLayout()
panel (constraints : BorderLayout.CENTER) {
gridBagLayout()
label(text : "Saved Tabs:", constraints : gbc(gridx : 0, gridy : 0))
scrollPane (constraints : gbc(gridx : 0, gridy : 1)) {
list(items : new ArrayList(settings.openTabs))
}
button(text : "Restore Session", constraints : gbc(gridx :0, gridy : 2), restoreSessionAction)
}
}
}
panel (constraints: "downloads window") {
gridLayout(rows : 1, cols : 1)
@@ -145,7 +185,7 @@ class MainFrameView {
panel {
borderLayout()
scrollPane (constraints : BorderLayout.CENTER) {
downloadsTable = table(id : "downloads-table", autoCreateRowSorter : true) {
downloadsTable = table(id : "downloads-table", autoCreateRowSorter : true, rowHeight : rowHeight) {
tableModel(list: model.downloads) {
closureColumn(header: "Name", preferredWidth: 300, type: String, read : {row -> row.downloader.file.getName()})
closureColumn(header: "Status", preferredWidth: 50, type: String, read : {row -> row.downloader.getCurrentState().toString()})
@@ -224,7 +264,7 @@ class MainFrameView {
panel (constraints : "shared files table") {
borderLayout()
scrollPane(constraints : BorderLayout.CENTER) {
table(id : "shared-files-table", autoCreateRowSorter: true) {
table(id : "shared-files-table", autoCreateRowSorter: true, rowHeight : rowHeight) {
tableModel(list : model.shared) {
closureColumn(header : "Name", preferredWidth : 500, type : String, read : {row -> row.getCachedPath()})
closureColumn(header : "Size", preferredWidth : 50, type : Long, read : {row -> row.getCachedLength() })
@@ -240,7 +280,7 @@ class MainFrameView {
scrollPane(constraints : BorderLayout.CENTER) {
def jtree = new JTree(model.sharedTree)
jtree.setCellRenderer(new SharedTreeRenderer())
tree(id : "shared-files-tree", rootVisible : false, expandsSelectedPaths: true, jtree)
tree(id : "shared-files-tree", rowHeight : rowHeight, rootVisible : false, expandsSelectedPaths: true, jtree)
}
}
}
@@ -270,7 +310,7 @@ class MainFrameView {
label("Uploads")
}
scrollPane (constraints : BorderLayout.CENTER) {
table(id : "uploads-table") {
table(id : "uploads-table", rowHeight : rowHeight) {
tableModel(list : model.uploads) {
closureColumn(header : "Name", type : String, read : {row -> row.uploader.getName() })
closureColumn(header : "Progress", type : String, read : { row ->
@@ -316,7 +356,7 @@ class MainFrameView {
label("Connections")
}
scrollPane(constraints : BorderLayout.CENTER) {
table(id : "connections-table") {
table(id : "connections-table", rowHeight : rowHeight) {
tableModel(list : model.connectionList) {
closureColumn(header : "Destination", preferredWidth: 250, type: String, read : { row -> row.destination.toBase32() })
closureColumn(header : "Direction", preferredWidth: 20, type: String, read : { row ->
@@ -335,7 +375,7 @@ class MainFrameView {
label("Incoming searches")
}
scrollPane(constraints : BorderLayout.CENTER) {
table(id : "searches-table") {
table(id : "searches-table", rowHeight : rowHeight) {
tableModel(list : model.searches) {
closureColumn(header : "Keywords", type : String, read : {
sanitized = it.search.replace('<', ' ')
@@ -368,7 +408,7 @@ class MainFrameView {
panel (border : etchedBorder()){
borderLayout()
scrollPane(constraints : BorderLayout.CENTER) {
table(id : "trusted-table", autoCreateRowSorter : true) {
table(id : "trusted-table", autoCreateRowSorter : true, rowHeight : rowHeight) {
tableModel(list : model.trusted) {
closureColumn(header : "Trusted Users", type : String, read : { it.getHumanReadableName() } )
}
@@ -384,7 +424,7 @@ class MainFrameView {
panel (border : etchedBorder()){
borderLayout()
scrollPane(constraints : BorderLayout.CENTER) {
table(id : "distrusted-table", autoCreateRowSorter : true) {
table(id : "distrusted-table", autoCreateRowSorter : true, rowHeight : rowHeight) {
tableModel(list : model.distrusted) {
closureColumn(header: "Distrusted Users", type : String, read : { it.getHumanReadableName() } )
}
@@ -403,7 +443,7 @@ class MainFrameView {
label(text : "Trust List Subscriptions")
}
scrollPane(constraints : BorderLayout.CENTER) {
table(id : "subscription-table", autoCreateRowSorter : true) {
table(id : "subscription-table", autoCreateRowSorter : true, rowHeight : rowHeight) {
tableModel(list : model.subscriptions) {
closureColumn(header : "Name", preferredWidth: 200, type: String, read : {it.persona.getHumanReadableName()})
closureColumn(header : "Trusted", preferredWidth : 20, type: Integer, read : {it.good.size()})
@@ -473,6 +513,10 @@ class MainFrameView {
}
}})
// search field
def searchField = builder.getVariable("search-field")
// downloads table
def downloadsTable = builder.getVariable("downloads-table")
def selectionModel = downloadsTable.getSelectionModel()
selectionModel.setSelectionMode(ListSelectionModel.SINGLE_SELECTION)
@@ -590,6 +634,8 @@ class MainFrameView {
model.addCommentButtonEnabled = selectedNode != null
})
sharedFilesTree.addTreeExpansionListener(expansionListener)
// searches table
def searchesTable = builder.getVariable("searches-table")
@@ -695,6 +741,9 @@ class MainFrameView {
// show tree by default
showSharedFilesTree.call()
// show search panel by default
showSearchWindow.call()
}
private static void showPopupMenu(JPopupMenu menu, MouseEvent event) {
@@ -839,10 +888,23 @@ class MainFrameView {
showPopupMenu(menu, e)
}
void showRestoreOrEmpty() {
def searchWindow = builder.getVariable("search window")
String id
if (!model.sessionRestored && !settings.openTabs.isEmpty())
id = model.connections > 0 ? "restore session" : "tabs-panel"
else
id = "tabs-panel"
searchWindow.getLayout().show(searchWindow, id)
}
def showSearchWindow = {
def cardsPanel = builder.getVariable("cards-panel")
cardsPanel.getLayout().show(cardsPanel, "search window")
showRestoreOrEmpty()
model.searchesPaneButtonEnabled = false
model.downloadsPaneButtonEnabled = true
model.uploadsPaneButtonEnabled = true
@@ -930,16 +992,31 @@ class MainFrameView {
public void refreshSharedFiles() {
def tree = builder.getVariable("shared-files-tree")
TreePath[] selectedPaths = tree.getSelectionPaths()
Set<TreePath> expanded = new HashSet<>(expansionListener.expandedPaths)
model.sharedTree.nodeStructureChanged(model.treeRoot)
expanded.each { tree.expandPath(it) }
tree.setSelectionPaths(selectedPaths)
builder.getVariable("shared-files-table").model.fireTableDataChanged()
}
private void closeApplication() {
Core core = application.getContext().get("core")
def tabbedPane = builder.getVariable("result-tabs")
settings.openTabs.clear()
int count = tabbedPane.getTabCount()
for (int i = 0; i < count; i++)
settings.openTabs.add(tabbedPane.getTitleAt(i))
File uiPropsFile = new File(core.home, "gui.properties")
uiPropsFile.withOutputStream { settings.write(it) }
def mainFrame = builder.getVariable("main-frame")
mainFrame.setVisible(false)
application.getWindowManager().findWindow("shutdown-window").setVisible(true)
Core core = application.getContext().get("core")
if (core != null) {
Thread t = new Thread({
core.shutdown()
@@ -948,4 +1025,19 @@ class MainFrameView {
t.start()
}
}
private static class TreeExpansions implements TreeExpansionListener {
private final Set<TreePath> expandedPaths = new HashSet<>()
@Override
public void treeExpanded(TreeExpansionEvent event) {
expandedPaths.add(event.getPath())
}
@Override
public void treeCollapsed(TreeExpansionEvent event) {
expandedPaths.remove(event.getPath())
}
}
}

View File

@@ -61,6 +61,7 @@ class OptionsView {
def excludeLocalResultCheckbox
def showSearchHashesCheckbox
def clearUploadsCheckbox
def storeSearchHistoryCheckbox
def inBwField
def outBwField
@@ -196,8 +197,17 @@ class OptionsView {
constraints : gbc(gridx : 3, gridy : 2, anchor : GridBagConstraints.LINE_END))
}
panel (border : titledBorder(title : "Other Settings", border : etchedBorder(), titlePosition : TitledBorder.TOP),
panel (border : titledBorder(title : "Search Settings", border : etchedBorder(), titlePosition : TitledBorder.TOP),
constraints : gbc(gridx : 0, gridy : 1, fill : GridBagConstraints.HORIZONTAL, weightx : 100)) {
gridBagLayout()
label(text : "Remember search history", constraints: gbc(gridx: 0, gridy:0, anchor : GridBagConstraints.LINE_START, weightx: 100))
storeSearchHistoryCheckbox = checkBox(selected : bind {model.storeSearchHistory},
constraints : gbc(gridx : 1, gridy:0, anchor : GridBagConstraints.LINE_END))
button(text : "Clear history", constraints : gbc(gridx : 1, gridy : 1, anchor : GridBagConstraints.LINE_END), clearHistoryAction)
}
panel (border : titledBorder(title : "Other Settings", border : etchedBorder(), titlePosition : TitledBorder.TOP),
constraints : gbc(gridx : 0, gridy : 2, fill : GridBagConstraints.HORIZONTAL, weightx : 100)) {
gridBagLayout()
label(text : "Automatically clear cancelled downloads", constraints: gbc(gridx: 0, gridy:0, anchor : GridBagConstraints.LINE_START, weightx: 100))
clearCancelledDownloadsCheckbox = checkBox(selected : bind {model.clearCancelledDownloads},
@@ -221,7 +231,7 @@ class OptionsView {
radioButton(text : "Exit", selected : bind {model.exitOnClose}, buttonGroup : closeBehaviorGroup, exitOnCloseAction)
}
}
panel (constraints : gbc(gridx: 0, gridy: 2, weighty: 100))
panel (constraints : gbc(gridx: 0, gridy: 3, weighty: 100))
}
bandwidth = builder.panel {
gridBagLayout()

View File

@@ -49,6 +49,7 @@ class SearchTabView {
def sequentialDownloadCheckbox
void initUI() {
int rowHeight = application.context.get("row-height")
builder.with {
def resultsTable
def sendersTable
@@ -59,7 +60,7 @@ class SearchTabView {
panel {
borderLayout()
scrollPane (constraints : BorderLayout.CENTER) {
sendersTable = table(id : "senders-table", autoCreateRowSorter : true) {
sendersTable = table(id : "senders-table", autoCreateRowSorter : true, rowHeight : rowHeight) {
tableModel(list : model.senders) {
closureColumn(header : "Sender", preferredWidth : 500, type: String, read : {row -> row.getHumanReadableName()})
closureColumn(header : "Results", preferredWidth : 20, type: Integer, read : {row -> model.sendersBucket[row].size()})
@@ -85,7 +86,7 @@ class SearchTabView {
panel {
borderLayout()
scrollPane (constraints : BorderLayout.CENTER) {
resultsTable = table(id : "results-table", autoCreateRowSorter : true) {
resultsTable = table(id : "results-table", autoCreateRowSorter : true, rowHeight : rowHeight) {
tableModel(list: model.results) {
closureColumn(header: "Name", preferredWidth: 350, type: String, read : {row -> row.name.replace('<','_')})
closureColumn(header: "Size", preferredWidth: 20, type: Long, read : {row -> row.size})

View File

@@ -29,6 +29,7 @@ class TrustListView {
void initUI() {
mainFrame = application.windowManager.findWindow("main-frame")
int rowHeight = application.context.get("row-height")
dialog = new JDialog(mainFrame, model.trustList.persona.getHumanReadableName(), true)
mainPanel = builder.panel {
borderLayout()
@@ -46,7 +47,7 @@ class TrustListView {
panel {
borderLayout()
scrollPane (constraints : BorderLayout.CENTER){
table(id : "trusted-table", autoCreateRowSorter : true) {
table(id : "trusted-table", autoCreateRowSorter : true, rowHeight : rowHeight) {
tableModel(list : model.trusted) {
closureColumn(header: "Trusted Users", type : String, read : {it.getHumanReadableName()})
closureColumn(header: "Your Trust", type : String, read : {model.trustService.getLevel(it.destination).toString()})
@@ -62,7 +63,7 @@ class TrustListView {
panel {
borderLayout()
scrollPane (constraints : BorderLayout.CENTER ){
table(id : "distrusted-table", autoCreateRowSorter : true) {
table(id : "distrusted-table", autoCreateRowSorter : true, rowHeight : rowHeight) {
tableModel(list : model.distrusted) {
closureColumn(header: "Distrusted Users", type : String, read : {it.getHumanReadableName()})
closureColumn(header: "Your Trust", type : String, read : {model.trustService.getLevel(it.destination).toString()})

View File

@@ -0,0 +1,14 @@
package com.muwire.gui
import java.awt.event.KeyEvent
import javax.swing.JComboBox
class SearchField extends JComboBox {
SearchField(SearchFieldModel model) {
super()
setEditable(true)
setModel(model)
setEditor(new SearchFieldEditor(model, this))
}
}

View File

@@ -0,0 +1,47 @@
package com.muwire.gui
import javax.swing.JTextField
import javax.swing.SwingUtilities
import javax.swing.event.DocumentEvent
import javax.swing.event.DocumentListener
import javax.swing.plaf.basic.BasicComboBoxEditor
class SearchFieldEditor extends BasicComboBoxEditor {
private final SearchFieldModel model
private final SearchField field
SearchFieldEditor(SearchFieldModel model, SearchField field) {
super()
this.model = model
this.field = field
def action = field.getAction()
field.setAction(null)
editor.setAction(action)
editor.getDocument().addDocumentListener(new DocumentListener() {
@Override
public void insertUpdate(DocumentEvent e) {
SwingUtilities.invokeLater({
field.hidePopup()
if (model.onKeyStroke(editor.text))
field.showPopup()
})
}
@Override
public void removeUpdate(DocumentEvent e) {
SwingUtilities.invokeLater({
field.hidePopup()
if (model.onKeyStroke(editor.text))
field.showPopup()
})
}
@Override
public void changedUpdate(DocumentEvent e) {
}
})
}
}

View File

@@ -0,0 +1,100 @@
package com.muwire.gui
import javax.swing.AbstractListModel
import javax.swing.MutableComboBoxModel
class SearchFieldModel extends AbstractListModel implements MutableComboBoxModel {
private final UISettings uiSettings
private final File settingsFile
private final List<String> objects = new ArrayList<>()
private String selectedObject
SearchFieldModel(UISettings uiSettings, File home) {
super()
this.uiSettings = uiSettings
this.settingsFile = new File(home, "gui.properties")
uiSettings.searchHistory.each { objects.add(it) }
fireIntervalAdded(this, 0, objects.size() - 1)
}
public void addElement(Object string) {
if (string == null)
return
if (uiSettings.storeSearchHistory) {
if (!uiSettings.searchHistory.add(string))
return
settingsFile.withOutputStream { uiSettings.write(it) }
}
objects.add(string);
fireIntervalAdded(this,objects.size()-1, objects.size()-1);
if ( objects.size() == 1 && selectedObject == null && string != null ) {
setSelectedItem( string );
}
}
boolean onKeyStroke(String selected) {
selectedObject = selected
if (selected == null || selected.length() == 0) {
objects.clear()
uiSettings.searchHistory.each { objects.add(it) }
return true
}
objects.clear()
Set<String> matching = new HashSet<>(uiSettings.searchHistory)
matching.retainAll { it.contains(selected) }
matching.each {
objects.add(it)
}
Collections.sort(objects)
if (!objects.isEmpty()) {
fireIntervalAdded(this, 0, objects.size() - 1)
return true
}
false
}
@Override
public void setSelectedItem(Object anObject) {
if ((selectedObject != null && !selectedObject.equals( anObject )) ||
selectedObject == null && anObject != null) {
selectedObject = anObject;
fireContentsChanged(this, -1, -1);
}
}
@Override
public Object getSelectedItem() {
selectedObject
}
@Override
public int getSize() {
objects.size()
}
@Override
public Object getElementAt(int index) {
if ( index >= 0 && index < objects.size() )
return objects.get(index);
else
return null;
}
@Override
public void removeElement(Object obj) {
}
@Override
public void insertElementAt(Object item, int index) {
}
@Override
public void removeElementAt(int index) {
}
}

View File

@@ -1,5 +1,7 @@
package com.muwire.gui
import com.muwire.core.util.DataUtil
class UISettings {
String lnf
@@ -14,6 +16,9 @@ class UISettings {
boolean closeWarning
boolean exitOnClose
boolean clearUploads
boolean storeSearchHistory
Set<String> searchHistory
Set<String> openTabs
UISettings(Properties props) {
lnf = props.getProperty("lnf", "system")
@@ -28,6 +33,10 @@ class UISettings {
closeWarning = Boolean.parseBoolean(props.getProperty("closeWarning","true"))
exitOnClose = Boolean.parseBoolean(props.getProperty("exitOnClose","false"))
clearUploads = Boolean.parseBoolean(props.getProperty("clearUploads","false"))
storeSearchHistory = Boolean.parseBoolean(props.getProperty("storeSearchHistory","true"))
searchHistory = DataUtil.readEncodedSet(props, "searchHistory")
openTabs = DataUtil.readEncodedSet(props, "openTabs")
}
void write(OutputStream out) throws IOException {
@@ -43,9 +52,12 @@ class UISettings {
props.setProperty("closeWarning", String.valueOf(closeWarning))
props.setProperty("exitOnClose", String.valueOf(exitOnClose))
props.setProperty("clearUploads", String.valueOf(clearUploads))
props.setProperty("storeSearchHistory", String.valueOf(storeSearchHistory))
if (font != null)
props.setProperty("font", font)
DataUtil.writeEncodedSet(searchHistory, "searchHistory", props)
DataUtil.writeEncodedSet(openTabs, "openTabs", props)
props.store(out, "UI Properties")
}

View File

@@ -0,0 +1,30 @@
############################################################
# Default Logging Configuration File
#
# You can use a different file by specifying a filename
# with the java.util.logging.config.file system property.
# For example java -Djava.util.logging.config.file=myfile
############################################################
############################################################
# Global properties
############################################################
# "handlers" specifies a comma separated list of log Handler
# classes. These handlers will be installed during VM startup.
# Note that these classes must be on the system classpath.
# By default we only configure a ConsoleHandler, which will only
# show messages at the INFO and above levels.
handlers= java.util.logging.FileHandler
# To also add the FileHandler, use the following line instead.
#handlers= java.util.logging.FileHandler, java.util.logging.ConsoleHandler
# Default global logging level.
# This specifies which kinds of events are logged across
# all loggers. For any given facility this global level
# can be overriden by a facility specific level
# Note that the ConsoleHandler also has a separate level
# setting to limit messages printed to the console.
.level= OFF