From 0256f4616d57dbf86be434959f5eb05d494f6a00 Mon Sep 17 00:00:00 2001 From: Mitchell Livingston Date: Mon, 18 Jun 2007 03:40:41 +0000 Subject: [PATCH] Merge file selection and torrent creation into the main branch. The new code for these features is under a new license. --- LICENSE | 14 +- NEWS | 17 + Transmission.xcodeproj/project.pbxproj | 66 +- beos/TRTransfer.cpp | 18 +- cli/transmissioncli.c | 81 +- daemon/server.c | 5 +- gtk/actions.c | 244 ++++ gtk/actions.h | 26 + gtk/hig.c | 38 +- gtk/hig.h | 36 +- gtk/ipc.c | 9 +- gtk/main.c | 573 +++++----- gtk/makemeta-ui.c | 300 +++++ gtk/makemeta-ui.h | 19 + gtk/my-valgrind.sh | 5 + gtk/torrent-inspector.c | 240 +++- gtk/torrent-inspector.h | 4 + gtk/tr_core.c | 38 +- gtk/tr_core.h | 2 +- gtk/tr_icon.c | 352 +----- gtk/tr_icon.h | 66 +- gtk/tr_prefs.c | 2 +- gtk/tr_torrent.c | 11 +- gtk/tr_window.c | 753 +++--------- gtk/tr_window.h | 58 +- gtk/transmission-gtk.1 | 3 +- gtk/ui.h | 66 ++ gtk/util.c | 154 +-- gtk/util.h | 49 +- libtransmission/choking.c | 15 +- libtransmission/completion.c | 384 ++++--- libtransmission/completion.h | 71 +- libtransmission/fastresume.c | 415 +++++++ libtransmission/fastresume.h | 419 +------ libtransmission/inout.c | 1005 +++++------------ libtransmission/internal.h | 9 +- libtransmission/ipcparse.c | 4 +- libtransmission/ipcparse.h | 2 +- libtransmission/makemeta.c | 498 ++++++++ libtransmission/makemeta.h | 91 ++ libtransmission/metainfo.c | 24 +- libtransmission/peer.c | 51 +- libtransmission/peermessages.h | 4 +- libtransmission/peerparse.h | 2 +- libtransmission/peerutils.h | 260 ++--- libtransmission/torrent.c | 295 +++-- libtransmission/tracker.c | 5 +- libtransmission/transmission.h | 109 +- libtransmission/utils.c | 292 +++-- libtransmission/utils.h | 107 +- macosx/CTGradient/CTGradientAdditions.h | 1 + macosx/CTGradient/CTGradientAdditions.m | 40 +- macosx/Controller.h | 6 +- macosx/Controller.m | 143 ++- macosx/CreatorWindowController.h | 57 + macosx/CreatorWindowController.m | 340 ++++++ macosx/Credits.rtf | 5 +- macosx/DragOverlayWindow.h | 6 +- macosx/DragOverlayWindow.m | 29 +- macosx/English.lproj/Creator.nib/classes.nib | 30 + macosx/English.lproj/Creator.nib/info.nib | 22 + .../Creator.nib/keyedobjects.nib | Bin 0 -> 13046 bytes .../English.lproj/InfoWindow.nib/classes.nib | 7 + macosx/English.lproj/InfoWindow.nib/info.nib | 2 +- .../InfoWindow.nib/keyedobjects.nib | Bin 46049 -> 48817 bytes macosx/English.lproj/MainMenu.nib/classes.nib | 3 +- macosx/English.lproj/MainMenu.nib/info.nib | 2 +- .../MainMenu.nib/keyedobjects.nib | Bin 54228 -> 54460 bytes macosx/FileBrowserCell.h | 3 + macosx/FileBrowserCell.m | 11 +- macosx/FileOutlineView.h | 1 + macosx/FileOutlineView.m | 80 +- macosx/IPCController.m | 3 +- macosx/Images/Create.png | Bin 0 -> 1256 bytes macosx/InfoWindowController.h | 7 +- macosx/InfoWindowController.m | 225 +++- macosx/PiecesView.h | 2 +- macosx/PiecesView.m | 2 + macosx/Spanish.lproj/InfoPlist.strings | 2 +- macosx/StringAdditions.m | 2 +- macosx/Torrent.h | 31 +- macosx/Torrent.m | 285 +++-- macosx/TorrentCell.h | 2 +- macosx/TorrentCell.m | 35 +- macosx/TorrentTableView.m | 7 +- .../Transmission Help.helpindex | Bin 33730 -> 31725 bytes macosx/Transmission Help/gfx/creation.png | Bin 0 -> 15049 bytes .../Transmission Help/gfx/fileselection.png | Bin 0 -> 41146 bytes macosx/Transmission Help/gfx/open.png | Bin 0 -> 19396 bytes macosx/Transmission Help/html/FAQ.html | 3 + macosx/Transmission Help/html/Index2.html | 18 +- macosx/Transmission Help/html/Speed.html | 6 +- macosx/Transmission Help/html/check.html | 18 +- .../html/gettingstarted.html | 57 +- macosx/Transmission Help/html/multiple.html | 6 +- macosx/Transmission Help/html/pffirewall.html | 6 +- macosx/Transmission Help/html/pfrouter.html | 6 +- .../Transmission Help/html/portforward.html | 18 +- macosx/Transmission Help/html/upnp.html | 7 +- macosx/Transmission Help/html/usingt.html | 42 + mk/gtk.mk | 7 +- mk/lib.mk | 8 +- 102 files changed, 5259 insertions(+), 3645 deletions(-) create mode 100644 gtk/actions.c create mode 100644 gtk/actions.h create mode 100644 gtk/makemeta-ui.c create mode 100644 gtk/makemeta-ui.h create mode 100755 gtk/my-valgrind.sh create mode 100644 gtk/ui.h create mode 100644 libtransmission/fastresume.c create mode 100644 libtransmission/makemeta.c create mode 100644 libtransmission/makemeta.h create mode 100644 macosx/CreatorWindowController.h create mode 100644 macosx/CreatorWindowController.m create mode 100644 macosx/English.lproj/Creator.nib/classes.nib create mode 100644 macosx/English.lproj/Creator.nib/info.nib create mode 100644 macosx/English.lproj/Creator.nib/keyedobjects.nib create mode 100644 macosx/Images/Create.png create mode 100644 macosx/Transmission Help/gfx/creation.png create mode 100644 macosx/Transmission Help/gfx/fileselection.png create mode 100644 macosx/Transmission Help/gfx/open.png create mode 100644 macosx/Transmission Help/html/usingt.html diff --git a/LICENSE b/LICENSE index 7c80bb340..c96446ed1 100644 --- a/LICENSE +++ b/LICENSE @@ -1,5 +1,15 @@ -The Transmission binaries and source code are distributed under the MIT -license. +The Transmission binaries and most of its source code is +distributed under the MIT license. + +Some files are copyrighted by Charles Kerr and are covered by +the GPL version 2. Works owned by the Transmission project +are granted a special exemption to clause 2(b) so that the bulk +of its code can remain under the MIT license. This exemption does +not extend to original or derived works not owned by the +Transmission project. + +The remainder of this text pertains only to the MIT-licensed +portions of Transmission. ----- Permission is hereby granted, free of charge, to any person obtaining diff --git a/NEWS b/NEWS index fc43f0d08..564c91438 100644 --- a/NEWS +++ b/NEWS @@ -1,5 +1,22 @@ NEWS file for Transmission +0.80 () + - Ability to selectively download and prioritize files + - Torrent file creation + - Partial licensing change -- see the LICENSE file for details + - Fix to UPnP + - Rechecking torrents is now done one torrent at a time + to avoid hammering the disk + - Many miscellaneous bugfixes and small improvements + - OS X: + + Overlay when dragging torrent files onto window + + Ability to set an amount of time to considered a transfer stalled + + Various smaller interface changes and improvements + - GTK: + + Interface significantly reworked to closer match the Mac version + + Torrent inspector + + Other minor improvements + 0.72 (2007/04/30) - Reset download/upload amounts when sending "started" - Fix rare XML parsing bug diff --git a/Transmission.xcodeproj/project.pbxproj b/Transmission.xcodeproj/project.pbxproj index df897eceb..9d674dbf8 100644 --- a/Transmission.xcodeproj/project.pbxproj +++ b/Transmission.xcodeproj/project.pbxproj @@ -7,6 +7,7 @@ objects = { /* Begin PBXBuildFile section */ + 002C9EE60C19CD2500C2F6F4 /* fastresume.c in Sources */ = {isa = PBXBuildFile; fileRef = 002C9EE50C19CD2500C2F6F4 /* fastresume.c */; }; 35B038130AC5B6EB00A10FDF /* ResumeNoWaitOn.png in Resources */ = {isa = PBXBuildFile; fileRef = 35B037F90AC5B53800A10FDF /* ResumeNoWaitOn.png */; }; 35B038140AC5B6EC00A10FDF /* ResumeNoWaitOff.png in Resources */ = {isa = PBXBuildFile; fileRef = 35B037FA0AC5B53800A10FDF /* ResumeNoWaitOff.png */; }; 4D043A7F090AE979009FEDA8 /* TransmissionDocument.icns in Resources */ = {isa = PBXBuildFile; fileRef = 4D043A7E090AE979009FEDA8 /* TransmissionDocument.icns */; }; @@ -104,11 +105,17 @@ A2A306620AAD24A80049E2AC /* UKMainThreadProxy.m in Sources */ = {isa = PBXBuildFile; fileRef = A2A3065A0AAD24A80049E2AC /* UKMainThreadProxy.m */; }; A2AA579D0ADFCAB400CA59F6 /* PiecesImageView.m in Sources */ = {isa = PBXBuildFile; fileRef = A2AA579B0ADFCAB400CA59F6 /* PiecesImageView.m */; }; A2AF1C390A3D0F6200F1575D /* FileOutlineView.m in Sources */ = {isa = PBXBuildFile; fileRef = A2AF1C370A3D0F6200F1575D /* FileOutlineView.m */; }; + A2BE9C520C1E4AF5002D16E6 /* makemeta.c in Sources */ = {isa = PBXBuildFile; fileRef = A2BE9C4E0C1E4ADA002D16E6 /* makemeta.c */; }; + A2BE9C530C1E4AF7002D16E6 /* makemeta.h in Headers */ = {isa = PBXBuildFile; fileRef = A2BE9C4F0C1E4ADA002D16E6 /* makemeta.h */; }; A2BF07910B066E0800757C92 /* SpeedLimitToTurtleIconTransformer.m in Sources */ = {isa = PBXBuildFile; fileRef = A2BF078F0B066E0800757C92 /* SpeedLimitToTurtleIconTransformer.m */; }; A2C655650A04FEDC00E9FD82 /* BottomBorder.png in Resources */ = {isa = PBXBuildFile; fileRef = A2C655640A04FEDC00E9FD82 /* BottomBorder.png */; }; A2D0E0490A54A97C003C72CF /* Bandwidth.png in Resources */ = {isa = PBXBuildFile; fileRef = A2D0E0480A54A97C003C72CF /* Bandwidth.png */; }; A2D4F0830A915F6600890C32 /* RedDot.tiff in Resources */ = {isa = PBXBuildFile; fileRef = A2D4F0820A915F6600890C32 /* RedDot.tiff */; }; A2D4F0850A915F7200890C32 /* GreenDot.tiff in Resources */ = {isa = PBXBuildFile; fileRef = A2D4F0840A915F7200890C32 /* GreenDot.tiff */; }; + A2DF37060C220D03006523C1 /* CreatorWindowController.h in CopyFiles */ = {isa = PBXBuildFile; fileRef = A2DF37040C220D03006523C1 /* CreatorWindowController.h */; }; + A2DF37070C220D03006523C1 /* CreatorWindowController.m in Sources */ = {isa = PBXBuildFile; fileRef = A2DF37050C220D03006523C1 /* CreatorWindowController.m */; }; + A2DF377C0C222E2D006523C1 /* Creator.nib in Resources */ = {isa = PBXBuildFile; fileRef = A2DF377A0C222E2D006523C1 /* Creator.nib */; }; + A2E9AA760C249AF400085DCF /* Create.png in Resources */ = {isa = PBXBuildFile; fileRef = A2E9AA750C249AF400085DCF /* Create.png */; }; A2F40AE40A361C00006B8288 /* Transmission.icns in Resources */ = {isa = PBXBuildFile; fileRef = 4D2784360905709500687951 /* Transmission.icns */; }; A2F6DB090A55F31C0058D1E5 /* SpeedLimitButton.png in Resources */ = {isa = PBXBuildFile; fileRef = A2F6DB070A55F31C0058D1E5 /* SpeedLimitButton.png */; }; A2FB057F0BFEB6800095564D /* DragOverlayView.m in Sources */ = {isa = PBXBuildFile; fileRef = A2FB057D0BFEB6800095564D /* DragOverlayView.m */; }; @@ -242,12 +249,14 @@ files = ( A261F1E40A69A1B10002815A /* Growl.framework in CopyFiles */, A24F19210A3A796800C9C145 /* Sparkle.framework in CopyFiles */, + A2DF37060C220D03006523C1 /* CreatorWindowController.h in CopyFiles */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ + 002C9EE50C19CD2500C2F6F4 /* fastresume.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; name = fastresume.c; path = libtransmission/fastresume.c; sourceTree = ""; }; 089C165DFE840E0CC02AAC07 /* English */ = {isa = PBXFileReference; fileEncoding = 10; lastKnownFileType = text.plist.strings; name = English; path = macosx/English.lproj/InfoPlist.strings; sourceTree = ""; }; 1058C7A1FEA54F0111CA2CBB /* Cocoa.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Cocoa.framework; path = /System/Library/Frameworks/Cocoa.framework; sourceTree = ""; }; 13E42FB307B3F0F600E4EEF1 /* CoreData.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreData.framework; path = /System/Library/Frameworks/CoreData.framework; sourceTree = ""; }; @@ -303,7 +312,7 @@ 4DFBC2DD09C0970D00D5C571 /* Torrent.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = Torrent.h; path = macosx/Torrent.h; sourceTree = ""; }; 4DFBC2DE09C0970D00D5C571 /* Torrent.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = Torrent.m; path = macosx/Torrent.m; sourceTree = ""; }; 8D1107310486CEB800E47090 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist; name = Info.plist; path = macosx/Info.plist; sourceTree = ""; }; - 8D1107320486CEB800E47090 /* Transmission.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Transmission.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 8D1107320486CEB800E47090 /* Transmission.app */ = {isa = PBXFileReference; includeInIndex = 0; lastKnownFileType = wrapper.application; path = Transmission.app; sourceTree = BUILT_PRODUCTS_DIR; }; A200B8390A2263BA007BBB1E /* InfoWindowController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = InfoWindowController.h; path = macosx/InfoWindowController.h; sourceTree = ""; }; A200B83A0A2263BA007BBB1E /* InfoWindowController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = InfoWindowController.m; path = macosx/InfoWindowController.m; sourceTree = ""; }; A200B9630A227FD0007BBB1E /* English */ = {isa = PBXFileReference; lastKnownFileType = wrapper.nib; name = English; path = macosx/English.lproj/InfoWindow.nib; sourceTree = ""; }; @@ -406,12 +415,18 @@ A2AA579B0ADFCAB400CA59F6 /* PiecesImageView.m */ = {isa = PBXFileReference; fileEncoding = 30; lastKnownFileType = sourcecode.c.objc; name = PiecesImageView.m; path = macosx/PiecesImageView.m; sourceTree = ""; }; A2AF1C360A3D0F6200F1575D /* FileOutlineView.h */ = {isa = PBXFileReference; fileEncoding = 30; lastKnownFileType = sourcecode.c.h; name = FileOutlineView.h; path = macosx/FileOutlineView.h; sourceTree = ""; }; A2AF1C370A3D0F6200F1575D /* FileOutlineView.m */ = {isa = PBXFileReference; fileEncoding = 30; lastKnownFileType = sourcecode.c.objc; name = FileOutlineView.m; path = macosx/FileOutlineView.m; sourceTree = ""; }; + A2BE9C4E0C1E4ADA002D16E6 /* makemeta.c */ = {isa = PBXFileReference; fileEncoding = 30; lastKnownFileType = sourcecode.c.c; name = makemeta.c; path = libtransmission/makemeta.c; sourceTree = ""; }; + A2BE9C4F0C1E4ADA002D16E6 /* makemeta.h */ = {isa = PBXFileReference; fileEncoding = 30; lastKnownFileType = sourcecode.c.h; name = makemeta.h; path = libtransmission/makemeta.h; sourceTree = ""; }; A2BF078E0B066E0800757C92 /* SpeedLimitToTurtleIconTransformer.h */ = {isa = PBXFileReference; fileEncoding = 30; lastKnownFileType = sourcecode.c.h; name = SpeedLimitToTurtleIconTransformer.h; path = macosx/SpeedLimitToTurtleIconTransformer.h; sourceTree = ""; }; A2BF078F0B066E0800757C92 /* SpeedLimitToTurtleIconTransformer.m */ = {isa = PBXFileReference; fileEncoding = 30; lastKnownFileType = sourcecode.c.objc; name = SpeedLimitToTurtleIconTransformer.m; path = macosx/SpeedLimitToTurtleIconTransformer.m; sourceTree = ""; }; A2C655640A04FEDC00E9FD82 /* BottomBorder.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = BottomBorder.png; path = macosx/Images/BottomBorder.png; sourceTree = ""; }; A2D0E0480A54A97C003C72CF /* Bandwidth.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = Bandwidth.png; path = macosx/Images/Bandwidth.png; sourceTree = ""; }; A2D4F0820A915F6600890C32 /* RedDot.tiff */ = {isa = PBXFileReference; lastKnownFileType = image.tiff; name = RedDot.tiff; path = macosx/Images/RedDot.tiff; sourceTree = ""; }; A2D4F0840A915F7200890C32 /* GreenDot.tiff */ = {isa = PBXFileReference; lastKnownFileType = image.tiff; name = GreenDot.tiff; path = macosx/Images/GreenDot.tiff; sourceTree = ""; }; + A2DF37040C220D03006523C1 /* CreatorWindowController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = CreatorWindowController.h; path = macosx/CreatorWindowController.h; sourceTree = ""; }; + A2DF37050C220D03006523C1 /* CreatorWindowController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = CreatorWindowController.m; path = macosx/CreatorWindowController.m; sourceTree = ""; }; + A2DF377B0C222E2D006523C1 /* Creator.nib */ = {isa = PBXFileReference; lastKnownFileType = wrapper.nib; name = Creator.nib; path = macosx/English.lproj/Creator.nib; sourceTree = ""; }; + A2E9AA750C249AF400085DCF /* Create.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = Create.png; path = macosx/Images/Create.png; sourceTree = ""; }; A2F6DB070A55F31C0058D1E5 /* SpeedLimitButton.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = SpeedLimitButton.png; path = macosx/Images/SpeedLimitButton.png; sourceTree = ""; }; A2F8951E0A2D4BA500ED2127 /* Credits.rtf */ = {isa = PBXFileReference; lastKnownFileType = text.rtf; name = Credits.rtf; path = macosx/Credits.rtf; sourceTree = ""; }; A2FB057C0BFEB6800095564D /* DragOverlayView.h */ = {isa = PBXFileReference; fileEncoding = 30; lastKnownFileType = sourcecode.c.h; name = DragOverlayView.h; path = macosx/DragOverlayView.h; sourceTree = ""; }; @@ -591,6 +606,8 @@ A21576090C0D449A0057A26A /* BezierPathAdditions.m */, A256588A0A9A695400E8A03B /* MessageWindowController.h */, A256588B0A9A695400E8A03B /* MessageWindowController.m */, + A2DF37040C220D03006523C1 /* CreatorWindowController.h */, + A2DF37050C220D03006523C1 /* CreatorWindowController.m */, E1B6FBF80C0D719B0015FE4D /* Info Window */, E1B6FBFD0C0D72430015FE4D /* Prefs Window */, E1B6FC000C0D72A00015FE4D /* Overlay Window */, @@ -646,6 +663,7 @@ 4D043A7E090AE979009FEDA8 /* TransmissionDocument.icns */, 4DF7500808A103AD007B0D70 /* Info.png */, 4DF7500708A103AD007B0D70 /* Open.png */, + A2E9AA750C249AF400085DCF /* Create.png */, 4DF7500908A103AD007B0D70 /* Remove.png */, 35B037F90AC5B53800A10FDF /* ResumeNoWaitOn.png */, 35B037FA0AC5B53800A10FDF /* ResumeNoWaitOff.png */, @@ -670,6 +688,7 @@ 29B97318FDCFA39411CA2CEA /* MainMenu.nib */, A200B9620A227FD0007BBB1E /* InfoWindow.nib */, A21567EB0A9A5034004DECD6 /* MessageWindow.nib */, + A2DF377A0C222E2D006523C1 /* Creator.nib */, A2C655640A04FEDC00E9FD82 /* BottomBorder.png */, A289EB0B0A33C56D00B082A3 /* ButtonBorder.png */, A28DBADB0A33C1D800F4B4A7 /* ActionButton.png */, @@ -713,6 +732,9 @@ 4D1838DC09DEC04A0047D688 /* libtransmission */ = { isa = PBXGroup; children = ( + A2BE9C4E0C1E4ADA002D16E6 /* makemeta.c */, + A2BE9C4F0C1E4ADA002D16E6 /* makemeta.h */, + 002C9EE50C19CD2500C2F6F4 /* fastresume.c */, A24D2A770C0A65C400A0ED9F /* ipcparse.c */, A24D2A780C0A65C400A0ED9F /* ipcparse.h */, BEFC1DEE0C07861A00B0BB3C /* xml.h */, @@ -938,6 +960,7 @@ BEFC1E5C0C07861A00B0BB3C /* bencode.h in Headers */, BEFC1E5E0C07861A00B0BB3C /* bsdqueue.h in Headers */, BEFC1E5F0C07861A00B0BB3C /* trcompat.h in Headers */, + A2BE9C530C1E4AF7002D16E6 /* makemeta.h in Headers */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1144,6 +1167,8 @@ A22A8D560AEEAFA5007E9CB9 /* Localizable.strings in Resources */, A24103070AF80E390001C8D0 /* FilterButtonPressedLeft.png in Resources */, A241528B0C0261B8007DD3B4 /* Globe.tiff in Resources */, + A2DF377C0C222E2D006523C1 /* Creator.nib in Resources */, + A2E9AA760C249AF400085DCF /* Create.png in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1201,6 +1226,8 @@ BEFC1E580C07861A00B0BB3C /* clients.c in Sources */, BEFC1E5A0C07861A00B0BB3C /* choking.c in Sources */, BEFC1E5D0C07861A00B0BB3C /* bencode.c in Sources */, + 002C9EE60C19CD2500C2F6F4 /* fastresume.c in Sources */, + A2BE9C520C1E4AF5002D16E6 /* makemeta.c in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1252,6 +1279,7 @@ A24D2A640C0A624600A0ED9F /* IPCController.m in Sources */, A24838820C0BA608005CC3FE /* FilterBarView.m in Sources */, A215760B0C0D449A0057A26A /* BezierPathAdditions.m in Sources */, + A2DF37070C220D03006523C1 /* CreatorWindowController.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1392,12 +1420,24 @@ name = PrefsWindow.nib; sourceTree = ""; }; + A2DF377A0C222E2D006523C1 /* Creator.nib */ = { + isa = PBXVariantGroup; + children = ( + A2DF377B0C222E2D006523C1 /* Creator.nib */, + ); + name = Creator.nib; + sourceTree = ""; + }; /* End PBXVariantGroup section */ /* Begin XCBuildConfiguration section */ 4D18389C09DEC01E0047D688 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { + ARCHS = ( + ppc, + i386, + ); CONFIGURATION_BUILD_DIR = "$(SRCROOT)/build/Debug"; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", @@ -1421,6 +1461,10 @@ 4DDBB71E09E16BF100284745 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { + ARCHS = ( + ppc, + i386, + ); CONFIGURATION_BUILD_DIR = "$(SRCROOT)/build/Debug"; LIBRARY_SEARCH_PATHS = "$(SRCROOT)/build/Debug"; OTHER_CFLAGS = ( @@ -1438,6 +1482,10 @@ 4DF0C599089918A300DD8943 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { + ARCHS = ( + ppc, + i386, + ); CONFIGURATION_BUILD_DIR = "$(SRCROOT)/macosx"; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", @@ -1473,6 +1521,10 @@ GCC_VERSION_ppc = 4.0; GCC_WARN_ABOUT_RETURN_TYPE = NO; GCC_WARN_SIGN_COMPARE = NO; + GCC_WARN_UNUSED_FUNCTION = NO; + GCC_WARN_UNUSED_LABEL = NO; + GCC_WARN_UNUSED_PARAMETER = NO; + GCC_WARN_UNUSED_VALUE = NO; GCC_WARN_UNUSED_VARIABLE = NO; MACOSX_DEPLOYMENT_TARGET_i386 = 10.4; MACOSX_DEPLOYMENT_TARGET_ppc = 10.4; @@ -1486,6 +1538,10 @@ BEFC1C0A0C07753800B0BB3C /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { + ARCHS = ( + ppc, + i386, + ); CONFIGURATION_BUILD_DIR = "$(SRCROOT)/build/Debug"; COPY_PHASE_STRIP = NO; GCC_DYNAMIC_NO_PIC = NO; @@ -1517,6 +1573,10 @@ BEFC1CF80C07822400B0BB3C /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { + ARCHS = ( + ppc, + i386, + ); CONFIGURATION_BUILD_DIR = "$(SRCROOT)/build/Debug"; COPY_PHASE_STRIP = NO; GCC_DYNAMIC_NO_PIC = NO; @@ -1550,6 +1610,10 @@ BEFC1D420C0783EE00B0BB3C /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { + ARCHS = ( + ppc, + i386, + ); CONFIGURATION_BUILD_DIR = "$(SRCROOT)/build/Debug"; COPY_PHASE_STRIP = NO; GCC_DYNAMIC_NO_PIC = NO; diff --git a/beos/TRTransfer.cpp b/beos/TRTransfer.cpp index e1586c93e..e0ff9f8a9 100644 --- a/beos/TRTransfer.cpp +++ b/beos/TRTransfer.cpp @@ -61,7 +61,7 @@ void TRTransfer::Update(BView *owner, const BFont *font) { * Returns a bool signaling the view is dirty after the update. * * The view is determined to be dirty if the transfer - * status, progress, eta or the "shade" (even or odd) + * status, percentDone, eta or the "shade" (even or odd) * position in the list changes from the previous state. * If the tr_stat_t is in fact different then the new, full * status is memcpy'd overtop the existing code. @@ -73,7 +73,7 @@ bool TRTransfer::UpdateStatus(tr_stat_t *stat, bool shade) { bool dirty = false; if (fStatusLock->Lock()) { if (fStatus->status != stat->status || - fStatus->progress != stat->progress || + fStatus->percentDone != stat->percentDone || fStatus->eta != stat->eta || fShade != shade) { @@ -124,21 +124,21 @@ void TRTransfer::DrawItem(BView *owner, BRect frame, bool) { owner->DrawString(fName->String(), textLoc); if (fStatus->status & TR_STATUS_PAUSE ) { - sprintf(fTimeStr, "Paused (%.2f %%)", 100 * fStatus->progress); + sprintf(fTimeStr, "Paused (%.2f %%)", 100 * fStatus->percentDone); } else if (fStatus->status & TR_STATUS_CHECK_WAIT ) { sprintf(fTimeStr, "Waiting To Check Existing Files (%.2f %%)", - 100 * fStatus->progress); + 100 * fStatus->percentDone); } else if (fStatus->status & TR_STATUS_CHECK ) { sprintf(fTimeStr, "Checking Existing Files (%.2f %%)", - 100 * fStatus->progress); + 100 * fStatus->percentDone); } else if (fStatus->status & TR_STATUS_DOWNLOAD) { if (fStatus->eta < 0 ) { sprintf(fTimeStr, "--:--:-- Remaining (%.2f %%Complete)", - 100 * fStatus->progress); + 100 * fStatus->percentDone); } else { sprintf(fTimeStr, "%02d:%02d:%02d Remaining (%.2f %%Complete)", fStatus->eta / 3600, (fStatus->eta / 60) % 60, - fStatus->eta % 60, 100 * fStatus->progress); + fStatus->eta % 60, 100 * fStatus->percentDone); } } else if (fStatus->status & TR_STATUS_SEED) { sprintf(fTimeStr, "Seeding"); @@ -214,8 +214,8 @@ void TRTransfer::DrawItem(BView *owner, BRect frame, bool) { } owner->FillRect(rect); - if (fStatus->progress != 0.0f) { - rect.right = rect.left + (float)ceil(fStatus->progress * (rect.Width() - 4)), + if (fStatus->percentDone != 0.0f) { + rect.right = rect.left + (float)ceil(fStatus->percentDone * (rect.Width() - 4)), // Bevel owner->SetHighColor(tint_color(fBarColor, B_LIGHTEN_2_TINT)); diff --git a/cli/transmissioncli.c b/cli/transmissioncli.c index efd9a09a0..d5eb247f9 100644 --- a/cli/transmissioncli.c +++ b/cli/transmissioncli.c @@ -29,27 +29,40 @@ #include #include #include +#include #ifdef SYS_BEOS #include #define usleep snooze #endif -#define USAGE \ -"Usage: %s [options] file.torrent [options]\n\n" \ -"Options:\n" \ -" -d, --download Maximum download rate (-1 = no limit, default = -1)\n"\ -" -f, --finish Command you wish to run on completion\n" \ -" -h, --help Print this help and exit\n" \ -" -i, --info Print metainfo and exit\n" \ -" -n --nat-traversal Attempt NAT traversal using NAT-PMP or UPnP IGD\n" \ -" -p, --port Port we should listen on (default = %d)\n" \ -" -s, --scrape Print counts of seeders/leechers and exit\n" \ -" -u, --upload Maximum upload rate (-1 = no limit, default = 20)\n" \ -" -v, --verbose Verbose level (0 to 2, default = 0)\n" +/* macro to shut up "unused parameter" warnings */ +#ifdef __GNUC__ +#define UNUSED __attribute__((unused)) +#else +#define UNUSED +#endif + +const char * USAGE = +"Usage: %s [options] file.torrent [options]\n\n" +"Options:\n" +" -c, --create-from Create torrent from the specified source file.\n" +" -a, --announce Used in conjunction with -c.\n" +" -r, --private Used in conjunction with -c.\n" +" -m, --comment Adds an optional comment when creating a torrent.\n" +" -d, --download Maximum download rate (-1 = no limit, default = -1)\n" +" -f, --finish Command you wish to run on completion\n" +" -h, --help Print this help and exit\n" +" -i, --info Print metainfo and exit\n" +" -n --nat-traversal Attempt NAT traversal using NAT-PMP or UPnP IGD\n" +" -p, --port Port we should listen on (default = %d)\n" +" -s, --scrape Print counts of seeders/leechers and exit\n" +" -u, --upload Maximum upload rate (-1 = no limit, default = 20)\n" +" -v, --verbose Verbose level (0 to 2, default = 0)\n"; static int showHelp = 0; static int showInfo = 0; static int showScrape = 0; +static int isPrivate = 0; static int verboseLevel = 0; static int bindPort = TR_DEFAULT_PORT; static int uploadLimit = 20; @@ -60,6 +73,9 @@ static sig_atomic_t gotsig = 0; static tr_torrent_t * tor; static char * finishCall = NULL; +static char * announce = NULL; +static char * sourceFile = NULL; +static char * comment = NULL; static int parseCommandLine ( int argc, char ** argv ); static void sigHandler ( int signal ); @@ -123,6 +139,20 @@ int main( int argc, char ** argv ) /* Initialize libtransmission */ h = tr_init( "cli" ); + if( sourceFile && *sourceFile ) /* creating a torrent */ + { + int ret; + tr_metainfo_builder_t* builder = tr_metaInfoBuilderCreate( h, sourceFile ); + tr_makeMetaInfo( builder, NULL, announce, comment, isPrivate ); + while( !builder->isDone ) { + usleep( 1 ); + printf( "." ); + } + ret = !builder->failed; + tr_metaInfoBuilderFree( builder ); + return ret; + } + /* Open and parse torrent file */ if( !( tor = tr_torrentInit( h, torrentPath, NULL, 0, &error ) ) ) { @@ -223,18 +253,18 @@ int main( int argc, char ** argv ) else if( s->status & TR_STATUS_CHECK_WAIT ) { chars = snprintf( string, sizeof string, - "Waiting to check files... %.2f %%", 100.0 * s->progress ); + "Waiting to check files... %.2f %%", 100.0 * s->percentDone ); } else if( s->status & TR_STATUS_CHECK ) { chars = snprintf( string, sizeof string, - "Checking files... %.2f %%", 100.0 * s->progress ); + "Checking files... %.2f %%", 100.0 * s->percentDone ); } else if( s->status & TR_STATUS_DOWNLOAD ) { chars = snprintf( string, sizeof string, "Progress: %.2f %%, %d peer%s, dl from %d (%.2f KB/s), " - "ul to %d (%.2f KB/s) [%s]", 100.0 * s->progress, + "ul to %d (%.2f KB/s) [%s]", 100.0 * s->percentDone, s->peersTotal, ( s->peersTotal == 1 ) ? "" : "s", s->peersUploading, s->rateDownload, s->peersDownloading, s->rateUpload, @@ -267,7 +297,7 @@ int main( int argc, char ** argv ) fprintf( stderr, "\n" ); } - if( tr_getFinished( tor ) ) + if( tr_getDone(tor) || tr_getComplete(tor) ) { result = system(finishCall); } @@ -302,16 +332,21 @@ static int parseCommandLine( int argc, char ** argv ) { { "help", no_argument, NULL, 'h' }, { "info", no_argument, NULL, 'i' }, { "scrape", no_argument, NULL, 's' }, + { "private", no_argument, NULL, 'r' }, { "verbose", required_argument, NULL, 'v' }, { "port", required_argument, NULL, 'p' }, { "upload", required_argument, NULL, 'u' }, { "download", required_argument, NULL, 'd' }, { "finish", required_argument, NULL, 'f' }, + { "create", required_argument, NULL, 'c' }, + { "comment", required_argument, NULL, 'm' }, + { "announce", required_argument, NULL, 'a' }, { "nat-traversal", no_argument, NULL, 'n' }, { 0, 0, 0, 0} }; int c, optind = 0; - c = getopt_long( argc, argv, "hisv:p:u:d:f:n", long_options, &optind ); + c = getopt_long( argc, argv, "hisrv:p:u:d:f:c:m:a:n:", + long_options, &optind ); if( c < 0 ) { break; @@ -327,6 +362,9 @@ static int parseCommandLine( int argc, char ** argv ) case 's': showScrape = 1; break; + case 'r': + isPrivate = 1; + break; case 'v': verboseLevel = atoi( optarg ); break; @@ -342,6 +380,15 @@ static int parseCommandLine( int argc, char ** argv ) case 'f': finishCall = optarg; break; + case 'c': + sourceFile = optarg; + break; + case 'm': + comment = optarg; + break; + case 'a': + announce = optarg; + break; case 'n': natTraversal = 1; break; diff --git a/daemon/server.c b/daemon/server.c index 5b782eaf3..50e9e1ca1 100644 --- a/daemon/server.c +++ b/daemon/server.c @@ -754,17 +754,14 @@ int addstat( benc_val_t * list, int id, int types ) { tr_stat_t * st; - tr_info_t * inf; st = torrent_stat( id ); if( NULL == st ) { return 0; } - inf = torrent_info( id ); - assert( NULL != inf ); - return ipc_addstat( list, id, inf, st, types ); + return ipc_addstat( list, id, st, types ); } void diff --git a/gtk/actions.c b/gtk/actions.c new file mode 100644 index 000000000..25a355237 --- /dev/null +++ b/gtk/actions.c @@ -0,0 +1,244 @@ +/* + * This file Copyright (C) 2007 Charles Kerr + * + * This file is licensed by the GPL version 2. Works owned by the + * Transmission project are granted a special exemption to clause 2(b) + * so that the bulk of its code can remain under the MIT license. + * This exemption does not extend to derived works not owned by + * the Transmission project. + */ + +#include +#include +#include +#include "transmission.h" +#include "torrent-inspector.h" +#include "img_icon_full.h" + +#ifdef __GNUC__ +#define UNUSED __attribute__((unused)) +#else +#define UNUSED +#endif + + +extern void doAction (const char * action_name, gpointer user_data ); + +static GtkActionGroup * myGroup = 0; + +static void action_cb ( GtkAction * a, gpointer user_data ) +{ + doAction ( gtk_action_get_name(a), user_data ); +} + +#if !GTK_CHECK_VERSION(2,8,0) +#define GTK_STOCK_INFO NULL +#endif + +static GtkRadioActionEntry priority_toggle_entries[] = +{ + { "priority-high", NULL, N_("_High"), NULL, NULL, TR_PRI_HIGH }, + { "priority-normal", NULL, N_("_Normal"), NULL, NULL, TR_PRI_NORMAL }, + { "priority-low", NULL, N_("_Low"), NULL, NULL, TR_PRI_LOW }, + { "priority-dnd", NULL, N_("_Don't Get"), NULL, NULL, TR_PRI_DND } +}; + +extern void set_selected_file_priority ( tr_priority_t ); + +static void +priority_changed_cb (GtkAction *action UNUSED, GtkRadioAction *current) +{ + const int priority = gtk_radio_action_get_current_value (current); + set_selected_file_priority ( priority ); +} + + +static GtkActionEntry entries[] = +{ + { "file-menu", NULL, N_("_File"), NULL, NULL, NULL }, + { "edit-menu", NULL, N_("_Edit"), NULL, NULL, NULL }, + { "help-menu", NULL, N_("_Help"), NULL, NULL, NULL }, + { "priority-menu", NULL, N_("_Priority"), NULL, NULL, NULL }, + { "add-torrent", GTK_STOCK_ADD, + N_("_Add"), "A", NULL, G_CALLBACK(action_cb) }, + { "start-torrent", GTK_STOCK_EXECUTE, + N_("_Start"), "S", NULL, G_CALLBACK(action_cb) }, + { "recheck-torrent", GTK_STOCK_REFRESH, + N_("Re_check"), NULL, NULL, G_CALLBACK(action_cb) }, + { "stop-torrent", GTK_STOCK_STOP, + N_("S_top"), "T", NULL, G_CALLBACK(action_cb) }, + { "remove-torrent", GTK_STOCK_REMOVE, + N_("_Remove"), "R", NULL, G_CALLBACK(action_cb) }, + { "create-torrent", GTK_STOCK_NEW, + N_("_Create New Torrent"), NULL, NULL, G_CALLBACK(action_cb) }, + { "close", GTK_STOCK_CLOSE, + N_("_Close"), "C", NULL, G_CALLBACK(action_cb) }, + { "quit", GTK_STOCK_QUIT, + N_("_Quit"), "Q", NULL, G_CALLBACK(action_cb) }, + { "edit-preferences", GTK_STOCK_PREFERENCES, + N_("Edit _Preferences"), NULL, NULL, G_CALLBACK(action_cb) }, + { "show-torrent-inspector", GTK_STOCK_PROPERTIES, + N_("_Torrent Info"), NULL, NULL, G_CALLBACK(action_cb) }, + { "show-about-dialog", GTK_STOCK_ABOUT, + N_("_About Transmission"), NULL, NULL, G_CALLBACK(action_cb) }, + { "show-debug-window", GTK_STOCK_INFO, + N_("Show _Debug Window"), NULL, NULL, G_CALLBACK(action_cb) }, + { "toggle-main-window", "ICON_TRANSMISSION", + N_("Show / Hide _Transmission"), NULL, NULL, G_CALLBACK(action_cb) } +}; + +static void +ensure_tooltip (GtkActionEntry * e) +{ + if( !e->tooltip && e->label ) + { + const char * src; + char *tgt; + e->tooltip = g_malloc( strlen( e->label ) + 1 ); + for( src=e->label, tgt=(char*)e->tooltip; *src; ++src ) + if( *src != '_' ) + *tgt++ = *src; + } +} + +typedef struct +{ + const guint8* raw; + const char * name; +} +BuiltinIconInfo; + +/* only one icon now... but room to grow ;) */ +const BuiltinIconInfo my_builtin_icons [] = +{ + { tr_icon_full, "ICON_TRANSMISSION" } +}; + +static void +register_my_icons ( void ) +{ + int i; + const int n = G_N_ELEMENTS( my_builtin_icons ); + GtkIconFactory * factory = gtk_icon_factory_new (); + gtk_icon_factory_add_default( factory ); + + for( i=0; inext ) + { + GtkActionGroup * action_group = GTK_ACTION_GROUP( l->data ); + GList *ait, *actions = gtk_action_group_list_actions( action_group ); + for( ait=actions; ait!=NULL; ait=ait->next ) + { + GtkAction * action = GTK_ACTION( ait->data ); + const char * name = gtk_action_get_name( action ); + g_hash_table_insert( key_to_action, g_strdup(name), action ); + } + g_list_free( actions ); + } +} + +static GtkAction* +get_action( const char* name ) +{ + ensure_action_map_loaded( myUIManager ); + return ( GtkAction* ) g_hash_table_lookup( key_to_action, name ); +} + +void +action_activate ( const char * name ) +{ + GtkAction * action = get_action( name ); + g_assert( action != NULL ); + gtk_action_activate( action ); +} + +void +action_sensitize( const char * name, gboolean b ) +{ + GtkAction * action = get_action( name ); + g_assert( action != NULL ); + g_object_set( action, "sensitive", b, NULL ); +} + +void +action_toggle( const char * name, gboolean b ) +{ + GtkAction * action = get_action( name ); + gtk_toggle_action_set_active( GTK_TOGGLE_ACTION(action), b ); +} + +GtkWidget* +action_get_widget( const char * path ) +{ + return gtk_ui_manager_get_widget( myUIManager, path ); +} diff --git a/gtk/actions.h b/gtk/actions.h new file mode 100644 index 000000000..8fa83ceb5 --- /dev/null +++ b/gtk/actions.h @@ -0,0 +1,26 @@ +/* + * This file Copyright (C) 2007 Charles Kerr + * + * This file is licensed by the GPL version 2. Works owned by the + * Transmission project are granted a special exemption to clause 2(b) + * so that the bulk of its code can remain under the MIT license. + * This exemption does not extend to derived works not owned by + * the Transmission project. + */ + +#ifndef TR_ACTIONS_H +#define TR_ACTIONS_H + +#include + +void actions_init ( GtkUIManager * ui_manager, gpointer callback_user_data ); + +void action_activate ( const char * name ); + +void action_sensitize ( const char * name, gboolean b ); + +void action_toggle ( const char * name, gboolean b ); + +GtkWidget* action_get_widget ( const char * path ); + +#endif diff --git a/gtk/hig.c b/gtk/hig.c index 7c04a7f38..ced059358 100644 --- a/gtk/hig.c +++ b/gtk/hig.c @@ -1,20 +1,11 @@ -/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */ /* - * Pan - A Newsreader for Gtk+ - * Copyright (C) 2002 Charles Kerr + * This file Copyright (C) 2007 Charles Kerr * - * 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; version 2 of the License. - * - * 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, write to the Free Software - * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * This file is licensed by the GPL version 2. Works owned by the + * Transmission project are granted a special exemption to clause 2(b) + * so that the bulk of its code can remain under the MIT license. + * This exemption does not extend to derived works not owned by + * the Transmission project. */ #include @@ -102,7 +93,7 @@ hig_workarea_add_label_w (GtkWidget * table, GtkWidget * l) { if (GTK_IS_MISC(l)) - gtk_misc_set_alignment (GTK_MISC(l), 0.0f, 0.0f); + gtk_misc_set_alignment (GTK_MISC(l), 0.0f, 0.5f); if (GTK_IS_LABEL(l)) gtk_label_set_use_markup (GTK_LABEL(l), TRUE); gtk_table_attach (GTK_TABLE(table), l, 1, 2, row, row+1, GTK_FILL, GTK_FILL, 0, 0); @@ -166,3 +157,18 @@ hig_workarea_finish (GtkWidget * table, gtk_widget_set_size_request (w, 0u, 6u); gtk_table_attach_defaults (GTK_TABLE(table), w, 0, 4, *row, *row+1); } + +void +hig_message_dialog_set_text (GtkMessageDialog * dialog, + const char * primary, + const char * secondary) +{ +#if GTK_CHECK_VERSION(2,6,0) + gtk_message_dialog_set_markup (dialog, primary); + gtk_message_dialog_format_secondary_text (dialog, "%s", secondary); +#else + char * pch = g_strdup_printf ("%s\n \n%s", primary, secondary); + gtk_message_dialog_set_markup (dialog, pch); + g_free (pch); +#endif +} diff --git a/gtk/hig.h b/gtk/hig.h index 152ee1821..f6c58f1c4 100644 --- a/gtk/hig.h +++ b/gtk/hig.h @@ -1,26 +1,12 @@ -/****************************************************************************** - * $Id:$ +/* + * This file Copyright (C) 2007 Charles Kerr * - * Copyright (c) 2007 Transmission authors and contributors - * - * 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, sublicense, - * and/or sell copies of the Software, and to permit persons to whom the - * Software is furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * 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. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER - * DEALINGS IN THE SOFTWARE. - *****************************************************************************/ + * This file is licensed by the GPL version 2. Works owned by the + * Transmission project are granted a special exemption to clause 2(b) + * so that the bulk of its code can remain under the MIT license. + * This exemption does not extend to derived works not owned by + * the Transmission project. + */ #ifndef __HIG_H__ #define __HIG_H__ @@ -92,6 +78,12 @@ void hig_workarea_finish (GtkWidget * table, int * row); +void +hig_message_dialog_set_text (GtkMessageDialog * dialog, + const char * primary, + const char * secondary); + + /** *** **/ diff --git a/gtk/ipc.c b/gtk/ipc.c index 98663b1ca..bfb4c4f92 100644 --- a/gtk/ipc.c +++ b/gtk/ipc.c @@ -788,18 +788,15 @@ static int addinfo( TrTorrent * tor, enum ipc_msg msgid, int torid, int types, benc_val_t * val ) { - tr_info_t * inf; - tr_stat_t * st; - - inf = tr_torrent_info( tor ); if( IPC_MSG_INFO == msgid ) { + tr_info_t * inf = tr_torrent_info( tor ); return ipc_addinfo( val, torid, inf, types ); } else { - st = tr_torrent_stat( tor ); - return ipc_addstat( val, torid, inf, st, types ); + tr_stat_t * st = tr_torrent_stat( tor ); + return ipc_addstat( val, torid, st, types ); } } diff --git a/gtk/main.c b/gtk/main.c index 4546a86df..21ef144bb 100644 --- a/gtk/main.c +++ b/gtk/main.c @@ -36,9 +36,11 @@ #include #include +#include "actions.h" #include "conf.h" #include "dialogs.h" #include "ipc.h" +#include "makemeta-ui.h" #include "msgwin.h" #include "torrent-inspector.h" #include "tr_cell_renderer_progress.h" @@ -48,12 +50,11 @@ #include "tr_torrent.h" #include "tr_window.h" #include "util.h" +#include "ui.h" #include "transmission.h" #include "version.h" -#include "img_icon_full.h" - /* time in seconds to wait for torrents to stop when exiting */ #define TRACKER_EXIT_TIMEOUT 10 @@ -69,9 +70,16 @@ #if GTK_CHECK_VERSION(2,8,0) #define SHOW_LICENSE static const char * LICENSE = -"The Transmission binaries and source code are distributed under the MIT " +"The Transmission binaries and most of its source code is distributed " "license. " "\n\n" +"Some files are copyrighted by Charles Kerr and are covered by " +"the GPL version 2. Works owned by the Transmission project " +"are granted a special exemption to clause 2(b) so that the bulk " +"of its code can remain under the MIT license. This exemption does " +"not extend to original or derived works not owned by the " +"Transmission project. " +"\n\n" "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 " @@ -95,7 +103,7 @@ static const char * LICENSE = struct cbdata { GtkWindow * wind; TrCore * core; - TrIcon * icon; + GtkWidget * icon; TrPrefs * prefs; guint timer; gboolean msgwinopen; @@ -109,68 +117,10 @@ struct exitdata { guint timer; }; -enum -{ - ACT_OPEN = 0, - ACT_START, - ACT_STOP, - ACT_DELETE, - ACT_SEPARATOR1, - ACT_INFO, - ACT_DEBUG, - ACT_ABOUT, - ACT_SEPARATOR2, - ACT_PREF, - ACT_SEPARATOR3, - ACT_CLOSE, - ACT_QUIT, - ACT_ICON, - ACTION_COUNT, -}; - -#if !GTK_CHECK_VERSION(2,8,0) -#define GTK_STOCK_INFO NULL -#endif - -struct -{ - const char * label; - const char * icon; - guint key; - int flags; - const char * tooltip; -} -actions[] = -{ - { NULL, GTK_STOCK_ADD, 'o', ACTF_WHEREVER | ACTF_ALWAYS, - N_("Add a new torrent") }, - { N_("Start"), GTK_STOCK_EXECUTE, 0, ACTF_WHEREVER | ACTF_INACTIVE, - N_("Start a torrent that is not running") }, - { NULL, GTK_STOCK_STOP, 0, ACTF_WHEREVER | ACTF_ACTIVE, - N_("Stop a torrent that is running") }, - { NULL, GTK_STOCK_REMOVE, 0, ACTF_WHEREVER | ACTF_WHATEVER, - N_("Remove a torrent") }, - { NULL, NULL, 0, ACTF_SEPARATOR, NULL }, - { NULL, GTK_STOCK_PROPERTIES, 0, ACTF_WHEREVER | ACTF_WHATEVER, - N_("Show additional information about a torrent") }, - { N_("Open debug window"), GTK_STOCK_INFO, 0, ACTF_MENU | ACTF_ALWAYS, - NULL }, - { NULL, GTK_STOCK_ABOUT, 0, ACTF_MENU | ACTF_ALWAYS, - N_("About Transmission") }, - { NULL, NULL, 0, ACTF_SEPARATOR, NULL }, - { NULL, GTK_STOCK_PREFERENCES, 0, ACTF_WHEREVER | ACTF_ALWAYS, - N_("Customize application behavior") }, - { NULL, NULL, 0, ACTF_SEPARATOR, NULL }, - { NULL, GTK_STOCK_CLOSE, 0, ACTF_MENU | ACTF_ALWAYS, - N_("Close the main window") }, - { NULL, GTK_STOCK_QUIT, 0, ACTF_MENU | ACTF_ALWAYS, - N_("Exit the program") }, - /* this isn't a terminator for the list, it's ACT_ICON */ - { NULL, NULL, 0, 0, NULL }, -}; - #define CBDATA_PTR "callback-data-pointer" +static GtkUIManager * myUIManager = NULL; + static sig_atomic_t global_sigcount = 0; static GList * @@ -178,17 +128,14 @@ readargs( int argc, char ** argv, gboolean * sendquit, gboolean * paused ); static gboolean sendremote( GList * files, gboolean sendquit ); static void -gtksetup( int * argc, char *** argv ); +gtksetup( int * argc, char *** argv, struct cbdata* ); static void -appsetup( TrWindow * wind, benc_val_t * state, GList * args, gboolean paused ); +appsetup( TrWindow * wind, benc_val_t * state, GList * args, + struct cbdata * , gboolean paused ); static void winsetup( struct cbdata * cbdata, TrWindow * wind ); static void -iconclick( struct cbdata * cbdata ); -static void makeicon( struct cbdata * cbdata ); -static gboolean -winclose( GtkWidget * widget, GdkEvent * event, gpointer gdata ); static void wannaquit( void * vdata ); static gboolean @@ -216,12 +163,8 @@ static gboolean updatemodel(gpointer gdata); static void boolwindclosed(GtkWidget *widget, gpointer gdata); -static void -windact(GtkWidget *widget, int action, gpointer gdata); static GList * getselection( struct cbdata * cbdata ); -static void -handleaction( struct cbdata *data, int action ); static void safepipe(void); @@ -230,10 +173,38 @@ setupsighandlers(void); static void fatalsig(int sig); +static void +accumulateStatusForeach (GtkTreeModel * model, + GtkTreePath * path UNUSED, + GtkTreeIter * iter, + gpointer accumulated_status) +{ + int status = 0; + gtk_tree_model_get( model, iter, MC_STAT, &status, -1 ); + *(int*)accumulated_status |= status; +} + +static void +refreshTorrentActions( GtkTreeSelection * s ) +{ + int status = 0; + gtk_tree_selection_selected_foreach( s, accumulateStatusForeach, &status ); + action_sensitize( "stop-torrent", (status & TR_STATUS_ACTIVE) != 0); + action_sensitize( "start-torrent", (status & TR_STATUS_INACTIVE) != 0); + action_sensitize( "remove-torrent", status != 0); + action_sensitize( "show-torrent-inspector", status != 0); +} + +static void +selectionChangedCB( GtkTreeSelection * s, gpointer unused UNUSED ) +{ + refreshTorrentActions( s ); +} + int main( int argc, char ** argv ) { - GtkWindow * mainwind; + struct cbdata * cbdata = g_new (struct cbdata, 1); char * err; benc_val_t * state; GList * argfiles; @@ -249,13 +220,15 @@ main( int argc, char ** argv ) didlock = sendremote( argfiles, sendquit ); } setupsighandlers(); /* set up handlers for fatal signals */ - gtksetup( &argc, &argv ); /* set up gtk and gettext */ + gtksetup( &argc, &argv, cbdata ); /* set up gtk and gettext */ if( ( didinit || cf_init( tr_getPrefsDirectory(), &err ) ) && ( didlock || cf_lock( &err ) ) ) { + GtkWindow * mainwind; + /* create main window now to be a parent to any error dialogs */ - mainwind = GTK_WINDOW( tr_window_new() ); + mainwind = GTK_WINDOW( tr_window_new( myUIManager ) ); /* try to load prefs and saved state */ cf_loadprefs( &err ); @@ -272,7 +245,7 @@ main( int argc, char ** argv ) } msgwin_loadpref(); /* set message level here before tr_init() */ - appsetup( TR_WINDOW( mainwind ), state, argfiles, startpaused ); + appsetup( mainwind, state, argfiles, cbdata, startpaused ); cf_freestate( state ); } else @@ -378,17 +351,21 @@ sendremote( GList * files, gboolean sendquit ) } static void -gtksetup( int * argc, char *** argv ) +gtksetup( int * argc, char *** argv, struct cbdata * callback_data ) { - GdkPixbuf * icon; - - gtk_init( argc, argv ); bindtextdomain( "transmission-gtk", LOCALEDIR ); bind_textdomain_codeset( "transmission-gtk", "UTF-8" ); textdomain( "transmission-gtk" ); g_set_application_name( _("Transmission") ); + gtk_init( argc, argv ); + + /* connect up the actions */ + myUIManager = gtk_ui_manager_new (); + actions_init ( myUIManager, callback_data ); + gtk_ui_manager_add_ui_from_string (myUIManager, fallback_ui_file, -1, NULL); + gtk_ui_manager_ensure_update (myUIManager); /* tweak some style properties in dialogs to get closer to the GNOME HiG */ gtk_rc_parse_string( @@ -400,19 +377,16 @@ gtksetup( int * argc, char *** argv ) "}\n" "widget \"TransmissionDialog\" style \"transmission-standard\"\n" ); - icon = gdk_pixbuf_new_from_inline( -1, tr_icon_full, FALSE, NULL ); - gtk_window_set_default_icon( icon ); - g_object_unref( icon ); + gtk_window_set_default_icon_name ( "ICON_TRANSMISSION" ); } static void -appsetup( TrWindow * wind, benc_val_t * state, GList * args, gboolean paused ) +appsetup( TrWindow * wind, benc_val_t * state, GList * args, + struct cbdata * cbdata, gboolean paused ) { - struct cbdata * cbdata; enum tr_torrent_action action; /* fill out cbdata */ - cbdata = g_new0( struct cbdata, 1 ); cbdata->wind = NULL; cbdata->core = tr_core_new(); cbdata->icon = NULL; @@ -459,96 +433,56 @@ appsetup( TrWindow * wind, benc_val_t * state, GList * args, gboolean paused ) updatemodel( cbdata ); /* show the window */ - tr_window_show( wind ); + gtk_widget_show( GTK_WIDGET(wind) ); +} + +static gboolean +winclose( GtkWidget * widget UNUSED, GdkEvent * event UNUSED, gpointer gdata ) +{ + struct cbdata * cbdata = (struct cbdata *) gdata; + + if( cbdata->icon != NULL ) + gtk_widget_hide( GTK_WIDGET( cbdata->wind ) ); + else + askquit( cbdata->wind, wannaquit, cbdata ); + + return TRUE; /* don't propagate event further */ +} + +static void +rowChangedCB( GtkTreeModel * model UNUSED, + GtkTreePath * path UNUSED, + GtkTreeIter * iter UNUSED, + gpointer sel) +{ + refreshTorrentActions( GTK_TREE_SELECTION(sel) ); } static void winsetup( struct cbdata * cbdata, TrWindow * wind ) { - int ii; - GtkWidget * drag; + GtkTreeModel * model; + GtkTreeSelection * sel; - g_assert( ACTION_COUNT == ALEN( actions ) ); g_assert( NULL == cbdata->wind ); cbdata->wind = GTK_WINDOW( wind ); - for( ii = 0; ii < ALEN( actions ); ii++ ) - { - tr_window_action_add( wind, ii, actions[ii].flags, - gettext( actions[ii].label ), actions[ii].icon, - gettext( actions[ii].tooltip ), - actions[ii].key ); - } - g_object_set( wind, "model", tr_core_model( cbdata->core ), - "double-click-action", ACT_INFO, NULL); - g_signal_connect( wind, "action", G_CALLBACK( windact ), cbdata ); + sel = tr_window_get_selection( cbdata->wind ); + g_signal_connect( sel, "changed", G_CALLBACK(selectionChangedCB), NULL ); + selectionChangedCB( sel, NULL ); + model = tr_core_model( cbdata->core ); + gtk_tree_view_set_model ( gtk_tree_selection_get_tree_view(sel), model ); + g_signal_connect( model, "row-changed", G_CALLBACK(rowChangedCB), sel ); g_signal_connect( wind, "delete-event", G_CALLBACK( winclose ), cbdata ); - g_object_get( wind, "drag-widget", &drag, NULL ); - setupdrag( drag, cbdata ); -} - -static void -iconclick( struct cbdata * cbdata ) -{ - GtkWidget * win; - - if( NULL != cbdata->wind ) - { - /* close window */ - winclose( NULL, NULL, cbdata ); - } - else - { - /* create window */ - win = tr_window_new(); - winsetup( cbdata, TR_WINDOW( win ) ); - tr_window_show( TR_WINDOW( win ) ); - } + setupdrag( GTK_WIDGET(wind), cbdata ); } static void makeicon( struct cbdata * cbdata ) { - TrIcon * icon; - int ii; - - if( NULL != cbdata->icon ) - { - return; - } - - icon = tr_icon_new(); - for( ii = 0; ii < ALEN( actions ); ii++ ) - { - tr_icon_action_add( TR_ICON( icon ), ii, actions[ii].flags, - gettext( actions[ii].label ), actions[ii].icon ); - } - g_object_set( icon, "activate-action", ACT_ICON, NULL); - g_signal_connect( icon, "action", G_CALLBACK( windact ), cbdata ); - - cbdata->icon = icon; -} - -static gboolean -winclose( GtkWidget * widget SHUTUP, GdkEvent * event SHUTUP, gpointer gdata ) -{ - struct cbdata * cbdata; - - cbdata = gdata; - - if( NULL != cbdata->icon && tr_icon_docked( cbdata->icon ) ) - { - gtk_widget_destroy( GTK_WIDGET( cbdata->wind ) ); - cbdata->wind = NULL; - } - else - { - askquit( cbdata->wind, wannaquit, cbdata ); - } - - /* don't propagate event further */ - return TRUE; + if( NULL == cbdata->icon ) + cbdata->icon = tr_icon_new( ); } static void @@ -862,6 +796,7 @@ prefschanged( TrCore * core SHUTUP, int id, gpointer data ) } else if( NULL != cbdata->icon ) { +g_message ("foo"); g_object_unref( cbdata->icon ); cbdata->icon = NULL; } @@ -909,7 +844,7 @@ updatemodel(gpointer gdata) { if( NULL != data->wind ) { tr_torrentRates( tr_core_handle( data->core ), &down, &up ); - tr_window_update( TR_WINDOW( data->wind ), down, up ); + tr_window_update( data->wind, down, up ); } /* update the message window */ @@ -925,32 +860,25 @@ boolwindclosed(GtkWidget *widget SHUTUP, gpointer gdata) { *preachy_gcc = FALSE; } -static void -windact( GtkWidget * wind SHUTUP, int action, gpointer gdata ) -{ - g_assert( 0 <= action ); - handleaction( gdata, action ); -} - /* returns a GList containing a GtkTreeRowReference to each selected row */ static GList * getselection( struct cbdata * cbdata ) { - GtkTreeSelection * sel; - GList * rows, * ii; - GtkTreeRowReference * ref; + GList * rows = NULL; - if( NULL == cbdata->wind ) + if( NULL != cbdata->wind ) { - return NULL; - } - g_object_get( cbdata->wind, "selection", &sel, NULL ); - rows = gtk_tree_selection_get_selected_rows( sel, NULL ); - for( ii = rows; NULL != ii; ii = ii->next ) - { - ref = gtk_tree_row_reference_new( tr_core_model( cbdata->core ), ii->data ); - gtk_tree_path_free( ii->data ); - ii->data = ref; + GList * ii; + GtkTreeSelection *s = tr_window_get_selection(cbdata->wind); + GtkTreeModel * model = tr_core_model( cbdata->core ); + rows = gtk_tree_selection_get_selected_rows( s, NULL ); + for( ii = rows; NULL != ii; ii = ii->next ) + { + GtkTreeRowReference * ref = gtk_tree_row_reference_new( + model, ii->data ); + gtk_tree_path_free( ii->data ); + ii->data = ref; + } } return rows; @@ -983,129 +911,168 @@ about ( void ) } static void -handleaction( struct cbdata * data, int act ) +startTorrentForeach (GtkTreeModel * model, + GtkTreePath * path UNUSED, + GtkTreeIter * iter, + gpointer data UNUSED) { - GList *rows, *ii; - GtkTreeModel * model; - GtkTreePath *path; - GtkTreeIter iter; - TrTorrent *tor; - int status; - gboolean changed; - GtkWidget * win; - - g_assert( ACTION_COUNT > act ); - - switch( act ) - { - case ACT_ABOUT: - about (); - return; - case ACT_OPEN: - makeaddwind( data->wind, data->core ); - return; - case ACT_PREF: - if( NULL != data->prefs ) - { - return; - } - data->prefs = tr_prefs_new_with_parent( G_OBJECT( data->core ), - data->wind ); - g_signal_connect( data->prefs, "destroy", - G_CALLBACK( gtk_widget_destroyed ), &data->prefs ); - gtk_widget_show( GTK_WIDGET( data->prefs ) ); - return; - case ACT_DEBUG: - if( !data->msgwinopen ) - { - data->msgwinopen = TRUE; - win = msgwin_create(); - g_signal_connect( win, "destroy", G_CALLBACK( boolwindclosed ), - &data->msgwinopen ); - } - return; - case ACT_ICON: - iconclick( data ); - return; - case ACT_CLOSE: - if( NULL != data->wind ) - { - winclose( NULL, NULL, data ); - } - return; - case ACT_QUIT: - askquit( data->wind, wannaquit, data ); - return; - case ACT_SEPARATOR1: - case ACT_SEPARATOR2: - case ACT_SEPARATOR3: - return; - case ACT_START: - case ACT_STOP: - case ACT_DELETE: - case ACT_INFO: - case ACTION_COUNT: - break; - } - - /* get a list of references to selected rows */ - rows = getselection( data ); - - model = tr_core_model( data->core ); - changed = FALSE; - for(ii = rows; NULL != ii; ii = ii->next) { - if(NULL != (path = gtk_tree_row_reference_get_path(ii->data)) && - gtk_tree_model_get_iter( model, &iter, path ) ) - { - gtk_tree_model_get( model , &iter, MC_TORRENT, &tor, - MC_STAT, &status, -1 ); - if( ACT_ISAVAIL( actions[act].flags, status ) ) - - { - switch( act ) - { - case ACT_START: - tr_torrent_start( tor ); - changed = TRUE; - break; - case ACT_STOP: - tr_torrent_stop( tor ); - changed = TRUE; - break; - case ACT_DELETE: - tr_core_delete_torrent( data->core, &iter ); - changed = TRUE; - break; - case ACT_INFO: - gtk_widget_show (torrent_inspector_new (data->wind, tor)); - break; - case ACT_OPEN: - case ACT_PREF: - case ACT_DEBUG: - case ACT_ICON: - case ACT_CLOSE: - case ACT_QUIT: - case ACT_SEPARATOR1: - case ACT_SEPARATOR2: - case ACT_SEPARATOR3: - case ACTION_COUNT: - break; - } - } - g_object_unref(tor); - } - if(NULL != path) - gtk_tree_path_free(path); - gtk_tree_row_reference_free(ii->data); - } - g_list_free(rows); - - if(changed) { - updatemodel(data); - tr_core_save( data->core ); - } + TrTorrent * tor = NULL; + gtk_tree_model_get( model, iter, MC_TORRENT, &tor, -1 ); + tr_torrent_start( tor ); + g_object_unref( G_OBJECT( tor ) ); } +static void +stopTorrentForeach (GtkTreeModel * model, + GtkTreePath * path UNUSED, + GtkTreeIter * iter, + gpointer data UNUSED) +{ + TrTorrent * tor = NULL; + gtk_tree_model_get( model, iter, MC_TORRENT, &tor, -1 ); + tr_torrent_stop( tor ); + g_object_unref( G_OBJECT( tor ) ); +} + +static void +showInfoForeach (GtkTreeModel * model, + GtkTreePath * path UNUSED, + GtkTreeIter * iter, + gpointer data UNUSED) +{ + TrTorrent * tor = NULL; + GtkWidget * w; + gtk_tree_model_get( model, iter, MC_TORRENT, &tor, -1 ); + w = torrent_inspector_new( GTK_WINDOW(data), tor ); + gtk_widget_show( w ); + g_object_unref( G_OBJECT( tor ) ); +} + +static void +recheckTorrentForeach (GtkTreeModel * model, + GtkTreePath * path UNUSED, + GtkTreeIter * iter, + gpointer data UNUSED) +{ + TrTorrent * gtor = NULL; + int status = 0; + tr_torrent_t * tor; + gtk_tree_model_get( model, iter, MC_TORRENT, >or, MC_STAT, &status, -1 ); + tor = tr_torrent_handle( gtor ); + if( status & TR_STATUS_ACTIVE ) + tr_torrentStop( tor ); + tr_torrentRemoveFastResume( tor ); + tr_torrentStart( tor ); + g_object_unref( G_OBJECT( gtor ) ); +} + +void +doAction ( const char * action_name, gpointer user_data ) +{ + struct cbdata * data = (struct cbdata *) user_data; + gboolean changed = FALSE; + + if (!strcmp (action_name, "add-torrent")) + { + makeaddwind( data->wind, data->core ); + } + else if (!strcmp (action_name, "start-torrent")) + { + GtkTreeSelection * s = tr_window_get_selection(data->wind); + gtk_tree_selection_selected_foreach( s, startTorrentForeach, NULL ); + changed |= gtk_tree_selection_count_selected_rows( s ) != 0; + } + else if (!strcmp (action_name, "stop-torrent")) + { + GtkTreeSelection * s = tr_window_get_selection(data->wind); + gtk_tree_selection_selected_foreach( s, stopTorrentForeach, NULL ); + changed |= gtk_tree_selection_count_selected_rows( s ) != 0; + } + else if (!strcmp (action_name, "recheck-torrent")) + { + GtkTreeSelection * s = tr_window_get_selection(data->wind); + gtk_tree_selection_selected_foreach( s, recheckTorrentForeach, NULL ); + changed |= gtk_tree_selection_count_selected_rows( s ) != 0; + } + else if (!strcmp (action_name, "show-torrent-inspector")) + { + GtkTreeSelection * s = tr_window_get_selection(data->wind); + gtk_tree_selection_selected_foreach( s, showInfoForeach, data->wind ); + } + else if (!strcmp (action_name, "create-torrent")) + { + GtkWidget * w = make_meta_ui( GTK_WINDOW( data->wind ), tr_core_handle( data->core ) ); + gtk_widget_show_all( w ); + } + else if (!strcmp (action_name, "remove-torrent")) + { + /* this modifies the model's contents, so can't use foreach */ + GList *l, *sel = getselection( data ); + GtkTreeModel *model = tr_core_model( data->core ); + for( l=sel; l!=NULL; l=l->next ) + { + GtkTreeIter iter; + GtkTreeRowReference * reference = (GtkTreeRowReference *) l->data; + GtkTreePath * path = gtk_tree_row_reference_get_path( reference ); + gtk_tree_model_get_iter( model, &iter, path ); + tr_core_delete_torrent( data->core, &iter ); + gtk_tree_row_reference_free( reference ); + changed = TRUE; + } + g_list_free( sel ); + } + else if (!strcmp (action_name, "close")) + { + if( data->wind != NULL ) + winclose( NULL, NULL, data ); + } + else if (!strcmp (action_name, "quit")) + { + askquit( data->wind, wannaquit, data ); + } + else if (!strcmp (action_name, "edit-preferences")) + { + if( NULL == data->prefs ) + { + data->prefs = tr_prefs_new_with_parent( G_OBJECT( data->core ), + data->wind ); + g_signal_connect( data->prefs, "destroy", + G_CALLBACK( gtk_widget_destroyed ), &data->prefs ); + gtk_widget_show( GTK_WIDGET( data->prefs ) ); + } + } + else if (!strcmp (action_name, "show-debug-window")) + { + if( !data->msgwinopen ) + { + GtkWidget * win = msgwin_create(); + g_signal_connect( win, "destroy", G_CALLBACK( boolwindclosed ), + &data->msgwinopen ); + data->msgwinopen = TRUE; + } + } + else if (!strcmp (action_name, "show-about-dialog")) + { + about(); + } + else if (!strcmp (action_name, "toggle-main-window")) + { + GtkWidget * w = GTK_WIDGET (data->wind); + if (GTK_WIDGET_VISIBLE(w)) + gtk_widget_hide (w); + else + gtk_window_present (GTK_WINDOW(w)); + } + else g_error ("Unhandled action: %s", action_name ); + + if(changed) + { + updatemodel( data ); + tr_core_save( data->core ); + } +} + + static void safepipe(void) { struct sigaction sa; diff --git a/gtk/makemeta-ui.c b/gtk/makemeta-ui.c new file mode 100644 index 000000000..d8c3b7ede --- /dev/null +++ b/gtk/makemeta-ui.c @@ -0,0 +1,300 @@ +/* + * This file Copyright (C) 2007 Charles Kerr + * + * This file is licensed by the GPL version 2. Works owned by the + * Transmission project are granted a special exemption to clause 2(b) + * so that the bulk of its code can remain under the MIT license. + * This exemption does not extend to derived works not owned by + * the Transmission project. + */ + +#include + +#include +#include + +#include "transmission.h" +#include "makemeta.h" + +#include "hig.h" +#include "makemeta-ui.h" +#include "util.h" + +#define UPDATE_INTERVAL_MSEC 200 + +typedef struct +{ + char torrent_name[2048]; + GtkWidget * size_lb; + GtkWidget * pieces_lb; + GtkWidget * announce_entry; + GtkWidget * comment_entry; + GtkWidget * progressbar; + GtkWidget * private_check; + GtkWidget * dialog; + GtkWidget * progress_dialog; + tr_metainfo_builder_t * builder; + tr_handle_t * handle; +} +MakeMetaUI; + +static void +freeMetaUI( gpointer p ) +{ + MakeMetaUI * ui = (MakeMetaUI *) p; + tr_metaInfoBuilderFree( ui->builder ); + memset( ui, ~0, sizeof(MakeMetaUI) ); + g_free( ui ); +} + +static void +progress_response_cb ( GtkDialog *d UNUSED, int response, gpointer user_data ) +{ + MakeMetaUI * ui = (MakeMetaUI *) user_data; + + if( response == GTK_RESPONSE_CANCEL ) + { + ui->builder->abortFlag = TRUE; + } + else + { + gtk_widget_destroy( ui->dialog ); + } +} + +static gboolean +refresh_cb ( gpointer user_data ) +{ + char buf[1024]; + double fraction; + MakeMetaUI * ui = (MakeMetaUI *) user_data; + GtkProgressBar * p = GTK_PROGRESS_BAR( ui->progressbar ); + + fraction = (double)ui->builder->pieceIndex / ui->builder->pieceCount; + gtk_progress_bar_set_fraction( p, fraction ); + g_snprintf( buf, sizeof(buf), "%s (%d%%)", ui->torrent_name, (int)(fraction*100 + 0.5)); + gtk_progress_bar_set_text( p, buf ); + + if( ui->builder->isDone ) + { + GtkWidget * w; + + if( ui->builder->failed ) + { + const char * reason = ui->builder->abortFlag + ? _("Torrent creation aborted.") + : _("Torrent creation failed."); + w = gtk_message_dialog_new (GTK_WINDOW(ui->progress_dialog), + GTK_DIALOG_DESTROY_WITH_PARENT, + GTK_MESSAGE_ERROR, + GTK_BUTTONS_CLOSE, reason ); + gtk_dialog_run( GTK_DIALOG( w ) ); + gtk_widget_destroy( ui->progress_dialog ); + } + else + { + GtkWidget * w = ui->progress_dialog; + gtk_window_set_title (GTK_WINDOW(ui->progress_dialog), _("Torrent Created")); + gtk_dialog_set_response_sensitive (GTK_DIALOG(w), GTK_RESPONSE_CANCEL, FALSE); + gtk_dialog_set_response_sensitive (GTK_DIALOG(w), GTK_RESPONSE_CLOSE, TRUE); + gtk_progress_bar_set_text( p, buf ); + } + } + + return !ui->builder->isDone; +} + +static void +remove_tag (gpointer tag) +{ + g_source_remove (GPOINTER_TO_UINT(tag)); /* stop the periodic refresh */ +} + +static void +response_cb( GtkDialog* d, int response, gpointer user_data ) +{ + MakeMetaUI * ui = (MakeMetaUI*) user_data; + GtkWidget *w, *p, *fr; + char *tmp; + char buf[1024]; + guint tag; + + if( response != GTK_RESPONSE_ACCEPT ) + { + gtk_widget_destroy( GTK_WIDGET( d ) ); + return; + } + + w = gtk_dialog_new_with_buttons( _("Making Torrent..."), + GTK_WINDOW(d), + GTK_DIALOG_DESTROY_WITH_PARENT, + GTK_STOCK_CANCEL, GTK_RESPONSE_CANCEL, + GTK_STOCK_CLOSE, GTK_RESPONSE_CLOSE, + NULL ); + g_signal_connect( w, "response", G_CALLBACK(progress_response_cb), ui ); + ui->progress_dialog = w; + gtk_dialog_set_response_sensitive (GTK_DIALOG(w), GTK_RESPONSE_CLOSE, FALSE); + + tmp = g_path_get_basename (ui->builder->top); + g_snprintf( ui->torrent_name, sizeof(ui->torrent_name), "%s.torrent", tmp ); + g_snprintf( buf, sizeof(buf), "%s (%d%%)", ui->torrent_name, 0); + p = ui->progressbar = gtk_progress_bar_new (); + gtk_progress_bar_set_text( GTK_PROGRESS_BAR(p), buf ); + fr = gtk_frame_new (NULL); + gtk_frame_set_shadow_type (GTK_FRAME(fr), GTK_SHADOW_NONE); + gtk_container_set_border_width( GTK_CONTAINER(fr), 20 ); + gtk_container_add (GTK_CONTAINER(fr), p); + gtk_box_pack_start_defaults( GTK_BOX(GTK_DIALOG(w)->vbox), fr ); + gtk_widget_show_all ( w ); + g_free( tmp ); + + tr_makeMetaInfo( ui->builder, + NULL, + gtk_entry_get_text( GTK_ENTRY( ui->announce_entry ) ), + gtk_entry_get_text( GTK_ENTRY( ui->comment_entry ) ), + gtk_toggle_button_get_active( GTK_TOGGLE_BUTTON( ui->private_check ) ) ); + + tag = g_timeout_add (UPDATE_INTERVAL_MSEC, refresh_cb, ui); + g_object_set_data_full (G_OBJECT(w), "tag", GUINT_TO_POINTER(tag), remove_tag); +} + +static void +file_mode_toggled_cb (GtkToggleButton *togglebutton, gpointer user_data) +{ + if( gtk_toggle_button_get_active( togglebutton ) ) + { + GtkFileChooserButton * w = GTK_FILE_CHOOSER_BUTTON(user_data); + gtk_file_chooser_set_action( GTK_FILE_CHOOSER( w ), GTK_FILE_CHOOSER_ACTION_OPEN ); + } +} + +static void +dir_mode_toggled_cb (GtkToggleButton *togglebutton, gpointer user_data) +{ + if( gtk_toggle_button_get_active( togglebutton ) ) + { + GtkFileChooserButton * w = GTK_FILE_CHOOSER_BUTTON(user_data); + gtk_file_chooser_set_action( GTK_FILE_CHOOSER( w ), GTK_FILE_CHOOSER_ACTION_SELECT_FOLDER ); + } +} + +/*** +**** +***/ + +static void +file_selection_changed_cb( GtkFileChooser *chooser, gpointer user_data ) +{ + MakeMetaUI * ui = (MakeMetaUI *) user_data; + char * pch; + char * filename; + char buf[512]; + size_t fileCount=0, totalSize=0; + size_t pieceCount=0, pieceSize=0; + + if( ui->builder ) { + tr_metaInfoBuilderFree( ui->builder ); + ui->builder = NULL; + } + + filename = gtk_file_chooser_get_filename( chooser ); + if( filename ) { + ui->builder = tr_metaInfoBuilderCreate( ui->handle, filename ); + g_free( filename ); + fileCount = ui->builder->fileCount; + totalSize = ui->builder->totalSize; + pieceCount = ui->builder->pieceCount; + pieceSize = ui->builder->pieceSize; + } + + pch = readablesize( totalSize ); + g_snprintf( buf, sizeof(buf), "%s; %lu %s", + pch, fileCount, + ngettext("file", "files", fileCount) ); + gtk_label_set_markup ( GTK_LABEL(ui->size_lb), buf ); + g_free( pch ); + + pch = readablesize( pieceSize ); + g_snprintf( buf, sizeof(buf), "%lu %s @ %s", + pieceCount, + ngettext("piece", "pieces", pieceCount), + pch ); + gtk_label_set_markup ( GTK_LABEL(ui->pieces_lb), buf ); + g_free( pch ); +} + +GtkWidget* +make_meta_ui( GtkWindow * parent, tr_handle_t * handle ) +{ + int row = 0; + GtkWidget *d, *t, *w, *h, *rb_file, *rb_dir; + char name[256]; + MakeMetaUI * ui = g_new0 ( MakeMetaUI, 1 ); + ui->handle = handle; + + d = gtk_dialog_new_with_buttons( _("Make a New Torrent"), + parent, + GTK_DIALOG_DESTROY_WITH_PARENT, + GTK_STOCK_CANCEL, GTK_RESPONSE_CANCEL, + GTK_STOCK_NEW, GTK_RESPONSE_ACCEPT, + NULL ); + g_signal_connect( d, "response", G_CALLBACK(response_cb), ui ); + g_object_set_data_full( G_OBJECT(d), "ui", ui, freeMetaUI ); + ui->dialog = d; + + t = hig_workarea_create (); + + hig_workarea_add_section_title (t, &row, _("Files")); + hig_workarea_add_section_spacer (t, row, 3); + + g_snprintf( name, sizeof(name), "%s:", _("File _Type")); + h = gtk_hbox_new( FALSE, GUI_PAD_SMALL ); + w = rb_dir = gtk_radio_button_new_with_mnemonic( NULL, _("Directory")); + gtk_box_pack_start ( GTK_BOX(h), w, FALSE, FALSE, 0 ); + w = rb_file = gtk_radio_button_new_with_mnemonic_from_widget( GTK_RADIO_BUTTON(w), _("Single File") ); + gtk_box_pack_start ( GTK_BOX(h), w, FALSE, FALSE, 0 ); + hig_workarea_add_row (t, &row, name, h, NULL); + + g_snprintf( name, sizeof(name), "%s:", _("_File")); + w = gtk_file_chooser_button_new( _("File or Directory to Add to the New Torrent"), + GTK_FILE_CHOOSER_ACTION_SELECT_FOLDER ); + g_signal_connect( w, "selection-changed", G_CALLBACK(file_selection_changed_cb), ui ); + g_signal_connect( rb_file, "toggled", G_CALLBACK(file_mode_toggled_cb), w ); + g_signal_connect( rb_dir, "toggled", G_CALLBACK(dir_mode_toggled_cb), w ); + hig_workarea_add_row (t, &row, name, w, NULL); + + g_snprintf( name, sizeof(name), "%s", _("No Files Selected")); + h = gtk_hbox_new( FALSE, GUI_PAD_SMALL ); + w = ui->size_lb = gtk_label_new (NULL); + gtk_label_set_markup ( GTK_LABEL(w), name ); + gtk_box_pack_start( GTK_BOX(h), w, FALSE, FALSE, 0 ); + w = ui->pieces_lb = gtk_label_new (NULL); + gtk_box_pack_end( GTK_BOX(h), w, FALSE, FALSE, 0 ); + w = gtk_alignment_new (0.0f, 0.0f, 0.0f, 0.0f); + gtk_widget_set_usize (w, 2 * GUI_PAD_BIG, 0); + gtk_box_pack_start_defaults ( GTK_BOX(h), w ); + hig_workarea_add_row (t, &row, "", h, NULL); + + + hig_workarea_add_section_divider( t, &row ); + hig_workarea_add_section_title (t, &row, _("Torrent")); + hig_workarea_add_section_spacer (t, row, 3); + + g_snprintf( name, sizeof(name), _("Private to this Tracker") ); + w = ui->private_check = hig_workarea_add_wide_checkbutton( t, &row, name, FALSE ); + + g_snprintf( name, sizeof(name), "%s:", _("Announce _URL")); + w = ui->announce_entry = gtk_entry_new( ); + gtk_entry_set_text(GTK_ENTRY(w), "http://"); + hig_workarea_add_row (t, &row, name, w, NULL ); + + g_snprintf( name, sizeof(name), "%s:", _("Commen_t")); + w = ui->comment_entry = gtk_entry_new( ); + hig_workarea_add_row (t, &row, name, w, NULL ); + + + gtk_window_set_default_size( GTK_WINDOW(d), 400u, 0u ); + gtk_box_pack_start_defaults( GTK_BOX(GTK_DIALOG(d)->vbox), t ); + gtk_widget_show_all( GTK_DIALOG(d)->vbox ); + return d; +} diff --git a/gtk/makemeta-ui.h b/gtk/makemeta-ui.h new file mode 100644 index 000000000..32e7a8714 --- /dev/null +++ b/gtk/makemeta-ui.h @@ -0,0 +1,19 @@ +/* + * This file Copyright (C) 2007 Charles Kerr + * + * This file is licensed by the GPL version 2. Works owned by the + * Transmission project are granted a special exemption to clause 2(b) + * so that the bulk of its code can remain under the MIT license. + * This exemption does not extend to derived works not owned by + * the Transmission project. + */ + +#ifndef MAKE_META_UI__H +#define MAKE_META_UI__H + +#include +#include "transmission.h" + +GtkWidget* make_meta_ui( GtkWindow * parent, tr_handle_t * handle ); + +#endif diff --git a/gtk/my-valgrind.sh b/gtk/my-valgrind.sh new file mode 100755 index 000000000..daff728c3 --- /dev/null +++ b/gtk/my-valgrind.sh @@ -0,0 +1,5 @@ +#/bin/sh +export G_SLICE=always-malloc +export G_DEBUG=gc-friendly +export GLIBCXX_FORCE_NEW=1 +valgrind --tool=memcheck --leak-check=full --leak-resolution=high --num-callers=64 --log-file=x-valgrind --show-reachable=yes ./transmission-gtk diff --git a/gtk/torrent-inspector.c b/gtk/torrent-inspector.c index 44d74f157..5f5b13866 100644 --- a/gtk/torrent-inspector.c +++ b/gtk/torrent-inspector.c @@ -26,8 +26,11 @@ #include #include #include + #include "transmission.h" #include "platform.h" /* for tr_getTorrentsDirectory */ + +#include "actions.h" #include "tr_torrent.h" #include "dot-icons.h" #include "hig.h" @@ -215,7 +218,7 @@ refresh_pieces (GtkWidget * da, GdkEventExpose * event UNUSED, gpointer gtor) gdk_draw_rectangle (da->window, gcs[GRAY], FALSE, grid_bounds.x, grid_bounds.y, - grid_bounds.width, grid_bounds.height-1); + grid_bounds.width-1, grid_bounds.height-1); g_free (pieces); g_free (completeness); @@ -596,7 +599,6 @@ static GtkWidget* peer_page_new ( TrTorrent * gtor ) gtk_scrolled_window_set_shadow_type (GTK_SCROLLED_WINDOW(w), GTK_SHADOW_IN); gtk_container_add (GTK_CONTAINER(w), v); - gtk_widget_set_usize (w, 500u, 0u); vbox = gtk_vbox_new (FALSE, GUI_PAD); @@ -802,7 +804,6 @@ refresh_activity (GtkWidget * top) { Activity * a = (Activity*) g_object_get_data (G_OBJECT(top), "activity-data"); const tr_stat_t * stat = tr_torrent_stat( a->gtor ); - const tr_info_t * info = tr_torrent_info( a->gtor ); guint64 size; char *pch; @@ -810,7 +811,7 @@ refresh_activity (GtkWidget * top) gtk_label_set_text (GTK_LABEL(a->state_lb), pch); g_free (pch); - size = info->totalSize - stat->left; + size = stat->downloadedValid; pch = readablesize (size); gtk_label_set_text (GTK_LABEL(a->valid_dl_lb), pch); g_free (pch); @@ -940,6 +941,7 @@ enum FC_KEY, FC_INDEX, FC_SIZE, + FC_PRIORITY, N_FILE_COLS }; @@ -948,20 +950,44 @@ typedef struct TrTorrent * gtor; GtkTreeModel * model; /* same object as store, but recast */ GtkTreeStore * store; /* same object as model, but recast */ + GtkTreeSelection * selection; } FileData; +static const char* +priorityToString( const int priority ) +{ + switch( priority ) { + case TR_PRI_HIGH: return _("High"); + case TR_PRI_NORMAL: return _("Normal"); + case TR_PRI_LOW: return _("Low"); + case TR_PRI_DND: return _("Don't Get"); + default: return "BUG!"; + } +} + +static tr_priority_t +stringToPriority( const char* str ) +{ + if( !strcmp( str, _( "High" ) ) ) return TR_PRI_HIGH; + if( !strcmp( str, _( "Low" ) ) ) return TR_PRI_LOW; + if( !strcmp( str, _( "Don't Get" ) ) ) return TR_PRI_DND;; + return TR_PRI_NORMAL; +} + static void -parsepath( GtkTreeStore * store, - GtkTreeIter * ret, - const char * path, - int index, - uint64_t size ) +parsepath( const tr_torrent_t * tor, + GtkTreeStore * store, + GtkTreeIter * ret, + const char * path, + int index, + uint64_t size ) { GtkTreeModel * model; GtkTreeIter * parent, start, iter; char * file, * lower, * mykey, *escaped=0; const char * stock; + int priority = 0; model = GTK_TREE_MODEL( store ); parent = NULL; @@ -969,7 +995,7 @@ parsepath( GtkTreeStore * store, if( 0 != strcmp( file, path ) ) { char * dir = g_path_get_dirname( path ); - parsepath( store, &start, dir, index, size ); + parsepath( tor, store, &start, dir, index, size ); parent = &start; g_free( dir ); } @@ -999,9 +1025,14 @@ parsepath( GtkTreeStore * store, index = -1; } + if (index != -1) + priority = tr_torrentGetFilePriority( tor, index ); + escaped = g_markup_escape_text (file, -1); gtk_tree_store_set( store, &iter, FC_INDEX, index, FC_LABEL, escaped, - FC_KEY, mykey, FC_STOCK, stock, FC_SIZE, size, -1 ); + FC_KEY, mykey, FC_STOCK, stock, + FC_PRIORITY, priorityToString(priority), + FC_SIZE, size, -1 ); done: g_free( escaped ); g_free( mykey ); @@ -1060,6 +1091,7 @@ updateprogress( GtkTreeModel * model, if( gtk_tree_model_iter_children( model, &iter, parent ) ) do { + int oldProg, newProg; guint64 subGot, subTotal; if (gtk_tree_model_iter_has_child( model, &iter ) ) @@ -1079,8 +1111,14 @@ updateprogress( GtkTreeModel * model, if (!subTotal) subTotal = 1; /* avoid div by zero */ g_assert (subGot <= subTotal); - gtk_tree_store_set (store, &iter, - FC_PROG, (int)(100.0*subGot/subTotal + 0.5), -1); + + /* why not just set it every time? + because that causes the "priorities" combobox to pop down */ + gtk_tree_model_get (model, &iter, FC_PROG, &oldProg, -1); + newProg = (int)(100.0*subGot/subTotal + 0.5); + if (oldProg != newProg) + gtk_tree_store_set (store, &iter, + FC_PROG, (int)(100.0*subGot/subTotal + 0.5), -1); gotSize += subGot; totalSize += subTotal; @@ -1091,18 +1129,142 @@ updateprogress( GtkTreeModel * model, *setmeTotalSize = totalSize; } +static GtkTreeModel* +priority_model_new (void) +{ + GtkTreeIter iter; + GtkListStore * store = gtk_list_store_new (2, G_TYPE_STRING, G_TYPE_INT); + gtk_list_store_append (store, &iter); + gtk_list_store_set (store, &iter, 0, _("High"), 1, TR_PRI_HIGH, -1); + gtk_list_store_append (store, &iter); + gtk_list_store_set (store, &iter, 0, _("Normal"), 1, TR_PRI_NORMAL, -1); + gtk_list_store_append (store, &iter); + gtk_list_store_set (store, &iter, 0, _("Low"), 1, TR_PRI_LOW, -1); + gtk_list_store_append (store, &iter); + gtk_list_store_set (store, &iter, 0, _("Don't Get"), 1, TR_PRI_DND, -1); + return GTK_TREE_MODEL (store); +} + +static void +refreshPriorityActions( GtkTreeSelection * sel ) +{ + GtkTreeIter iter; + GtkTreeModel * model; + const gboolean has_selection = gtk_tree_selection_get_selected( sel, &model, &iter ); + + action_sensitize ( "priority-high", has_selection ); + action_sensitize ( "priority-normal", has_selection ); + action_sensitize ( "priority-low", has_selection ); + action_sensitize ( "priority-dnd", has_selection ); + + if( has_selection ) + { + /* set the action priority base on the model's values */ + char * pch = NULL; + const char * key; + gtk_tree_model_get( model, &iter, FC_PRIORITY, &pch, -1 ); + switch( stringToPriority( pch ) ) { + case TR_PRI_HIGH: key = "priority-high"; break; + case TR_PRI_LOW: key = "priority-low"; break; + case TR_PRI_DND: key = "priority-dnd"; break; + default: key = "priority-normal"; break; + } + action_toggle( key, TRUE ); + g_free( pch ); + } +} + +static void +set_priority (GtkTreeSelection * selection, + GtkTreeStore * store, + GtkTreeIter * iter, + tr_torrent_t * tor, + int priority_val, + const char * priority_str) +{ + int index; + GtkTreeIter child; + + gtk_tree_model_get( GTK_TREE_MODEL(store), iter, FC_INDEX, &index, -1 ); + if (index >= 0) + tr_torrentSetFilePriority( tor, index, priority_val ); + gtk_tree_store_set( store, iter, FC_PRIORITY, priority_str, -1 ); + + if( gtk_tree_model_iter_children( GTK_TREE_MODEL(store), &child, iter ) ) do + set_priority( selection, store, &child, tor, priority_val, priority_str ); + while( gtk_tree_model_iter_next( GTK_TREE_MODEL(store), &child ) ); + + refreshPriorityActions( selection ); +} + +static void +priority_changed_cb (GtkCellRendererText * cell UNUSED, + const gchar * path, + const gchar * value, + void * file_data) +{ + GtkTreeIter iter; + FileData * d = (FileData*) file_data; + if (gtk_tree_model_get_iter_from_string (d->model, &iter, path)) + { + tr_torrent_t * tor = tr_torrent_handle( d->gtor ); + const tr_priority_t priority = stringToPriority( value ); + set_priority( d->selection, d->store, &iter, tor, priority, value ); + } +} + +/* FIXME: NULL this back out when popup goes down */ +static GtkWidget * popupView = NULL; + +static void +on_popup_menu ( GtkWidget * view, GdkEventButton * event ) +{ + GtkWidget * menu = action_get_widget ( "/file-popup" ); + popupView = view; + gtk_menu_popup (GTK_MENU(menu), NULL, NULL, NULL, NULL, + (event ? event->button : 0), + (event ? event->time : 0)); +} + +static void +fileSelectionChangedCB( GtkTreeSelection * sel, gpointer unused UNUSED ) +{ + refreshPriorityActions( sel ); +} + +void +set_selected_file_priority ( tr_priority_t priority_val ) +{ + if( popupView && GTK_IS_TREE_VIEW(popupView) ) + { + GtkTreeView * view = GTK_TREE_VIEW( popupView ); + tr_torrent_t * tor = (tr_torrent_t*) + g_object_get_data (G_OBJECT(view), "torrent-handle"); + const char * priority_str = priorityToString( priority_val ); + GtkTreeModel * model; + GtkTreeIter iter; + GtkTreeSelection * sel = gtk_tree_view_get_selection (view); + gtk_tree_selection_get_selected( sel, &model, &iter ); + + set_priority( sel, GTK_TREE_STORE(model), &iter, + tor, priority_val, priority_str ); + } +} + GtkWidget * file_page_new ( TrTorrent * gtor ) { - GtkWidget * ret; - FileData * data; - tr_info_t * inf; - GtkTreeStore * store; - int ii; - GtkWidget * view, * scroll; - GtkCellRenderer * rend; - GtkTreeViewColumn * col; - GtkTreeSelection * sel; + GtkWidget * ret; + FileData * data; + tr_info_t * inf; + tr_torrent_t * tor; + GtkTreeStore * store; + int ii; + GtkWidget * view, * scroll; + GtkCellRenderer * rend; + GtkTreeViewColumn * col; + GtkTreeSelection * sel; + GtkTreeModel * model; store = gtk_tree_store_new ( N_FILE_COLS, G_TYPE_STRING, /* stock */ @@ -1110,13 +1272,15 @@ file_page_new ( TrTorrent * gtor ) G_TYPE_INT, /* prog [0..100] */ G_TYPE_STRING, /* key */ G_TYPE_INT, /* index */ - G_TYPE_UINT64); /* size */ + G_TYPE_UINT64, /* size */ + G_TYPE_STRING); /* priority */ /* set up the model */ + tor = tr_torrent_handle( gtor ); inf = tr_torrent_info( gtor ); for( ii = 0; ii < inf->fileCount; ii++ ) { - parsepath( store, NULL, STRIPROOT( inf->files[ii].name ), + parsepath( tor, store, NULL, STRIPROOT( inf->files[ii].name ), ii, inf->files[ii].length ); } getdirtotals( store, NULL ); @@ -1125,6 +1289,12 @@ file_page_new ( TrTorrent * gtor ) /* create the view */ view = gtk_tree_view_new_with_model( GTK_TREE_MODEL( store ) ); + g_object_set_data (G_OBJECT(view), "torrent-handle", tor ); + g_signal_connect( view, "popup-menu", + G_CALLBACK(on_popup_menu), NULL ); + g_signal_connect( view, "button-press-event", + G_CALLBACK(on_tree_view_button_pressed), on_popup_menu); + /* add file column */ col = GTK_TREE_VIEW_COLUMN (g_object_new (GTK_TYPE_TREE_VIEW_COLUMN, @@ -1150,6 +1320,24 @@ file_page_new ( TrTorrent * gtor ) sel = gtk_tree_view_get_selection( GTK_TREE_VIEW( view ) ); gtk_tree_view_expand_all( GTK_TREE_VIEW( view ) ); gtk_tree_view_set_search_column( GTK_TREE_VIEW( view ), FC_LABEL ); + g_signal_connect( sel, "changed", G_CALLBACK(fileSelectionChangedCB), NULL ); + fileSelectionChangedCB( sel, NULL ); + + + /* add priority column */ + model = priority_model_new (); + col = gtk_tree_view_column_new (); + gtk_tree_view_column_set_title (col, _("Priority")); + rend = gtk_cell_renderer_combo_new (); + gtk_tree_view_column_pack_start (col, rend, TRUE); + g_object_set (G_OBJECT(rend), "model", model, + "editable", TRUE, + "has-entry", FALSE, + "text-column", 0, + NULL); + g_object_unref (G_OBJECT(model)); + gtk_tree_view_column_add_attribute (col, rend, "text", FC_PRIORITY); + gtk_tree_view_append_column( GTK_TREE_VIEW( view ), col ); /* create the scrolled window and stick the view in it */ scroll = gtk_scrolled_window_new( NULL, NULL ); @@ -1166,7 +1354,9 @@ file_page_new ( TrTorrent * gtor ) data->gtor = gtor; data->model = GTK_TREE_MODEL(store); data->store = store; + data->selection = gtk_tree_view_get_selection( GTK_TREE_VIEW( view ) ); g_object_set_data_full (G_OBJECT(ret), "file-data", data, g_free); + g_signal_connect (G_OBJECT(rend), "edited", G_CALLBACK(priority_changed_cb), data); return ret; } @@ -1271,7 +1461,7 @@ options_page_new ( TrTorrent * gtor ) hig_workarea_add_section_title (t, &row, _("Seeding")); hig_workarea_add_section_spacer (t, row, 1); - tb = gtk_check_button_new_with_mnemonic (_("Stop Seeding at Ratio:")); + tb = gtk_check_button_new_with_mnemonic (_("_Stop Seeding at Ratio:")); gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON(tb), gtor->seeding_cap_enabled); g_signal_connect (tb, "toggled", G_CALLBACK(seeding_cap_toggled_cb), gtor); a = (GtkAdjustment*) gtk_adjustment_new (gtor->seeding_cap, 0.0, G_MAXDOUBLE, 1, 1, 1); diff --git a/gtk/torrent-inspector.h b/gtk/torrent-inspector.h index e25b59be4..b575a151d 100644 --- a/gtk/torrent-inspector.h +++ b/gtk/torrent-inspector.h @@ -28,6 +28,10 @@ #include #include "tr_torrent.h" +struct TrTorrent; + GtkWidget* torrent_inspector_new ( GtkWindow * parent, TrTorrent * tor ); +void set_selected_file_priority ( tr_priority_t ); + #endif /* TG_PREFS_H */ diff --git a/gtk/tr_core.c b/gtk/tr_core.c index 01ecb4fa7..ac5810689 100644 --- a/gtk/tr_core.c +++ b/gtk/tr_core.c @@ -218,8 +218,8 @@ tr_core_init( GTypeInstance * instance, gpointer g_class SHUTUP ) { /* info->name, info->totalSize, info->hashString, status, */ G_TYPE_STRING, G_TYPE_UINT64, G_TYPE_STRING, G_TYPE_INT, - /* error, errorString, progress, rateDownload, rateUpload, */ - G_TYPE_INT, G_TYPE_STRING, G_TYPE_FLOAT, G_TYPE_FLOAT, G_TYPE_FLOAT, + /* error, errorString, percentComplete, percentDone, rateDownload, rateUpload, */ + G_TYPE_INT, G_TYPE_STRING, G_TYPE_FLOAT, G_TYPE_FLOAT, G_TYPE_FLOAT, G_TYPE_FLOAT, /* eta, peersTotal, peersUploading, peersDownloading, seeders, */ G_TYPE_INT, G_TYPE_INT, G_TYPE_INT, G_TYPE_INT, G_TYPE_INT, /* leechers, completedFromTracker, downloaded, uploaded */ @@ -264,16 +264,13 @@ tr_core_dispose( GObject * obj ) #endif /* sever all remaining torrents in the model */ - if( gtk_tree_model_get_iter_first( self->model, &iter ) ) + if( gtk_tree_model_get_iter_first( self->model, &iter ) ) do { - do - { - gtk_tree_model_get( self->model, &iter, MC_TORRENT, &tor, -1 ); - tr_torrent_sever( tor ); - g_object_unref( tor ); - } - while( gtk_tree_model_iter_next( self->model, &iter ) ); + gtk_tree_model_get( self->model, &iter, MC_TORRENT, &tor, -1 ); + tr_torrent_sever( tor ); + g_object_unref( tor ); } + while( gtk_tree_model_iter_next( self->model, &iter ) ); g_object_unref( self->model ); /* sever and unref all remaining zombie torrents */ @@ -308,27 +305,17 @@ tr_core_new( void ) GtkTreeModel * tr_core_model( TrCore * self ) { - TR_IS_CORE( self ); + g_return_val_if_fail (TR_IS_CORE(self), NULL); - if( self->disposed ) - { - return NULL; - } - - return self->model; + return self->disposed ? NULL : self->model; } tr_handle_t * tr_core_handle( TrCore * self ) { - TR_IS_CORE( self ); + g_return_val_if_fail (TR_IS_CORE(self), NULL); - if( self->disposed ) - { - return NULL; - } - - return self->handle; + return self->disposed ? NULL : self->handle; } void @@ -746,7 +733,8 @@ tr_core_update( TrCore * self ) MC_STAT, st->status, MC_ERR, st->error, MC_TERR, st->errorString, - MC_PROG, st->progress, + MC_PROG_C, st->percentComplete, + MC_PROG_D, st->percentDone, MC_DRATE, st->rateDownload, MC_URATE, st->rateUpload, MC_ETA, st->eta, diff --git a/gtk/tr_core.h b/gtk/tr_core.h index d31569937..22acc88e9 100644 --- a/gtk/tr_core.h +++ b/gtk/tr_core.h @@ -188,7 +188,7 @@ tr_core_set_pref_int( TrCore * self, int id, int val ); /* keep this in sync with the type array in tr_core_init() in tr_core.c */ enum { MC_NAME, MC_SIZE, MC_HASH, MC_STAT, MC_ERR, MC_TERR, - MC_PROG, MC_DRATE, MC_URATE, MC_ETA, MC_PEERS, + MC_PROG_C, MC_PROG_D, MC_DRATE, MC_URATE, MC_ETA, MC_PEERS, MC_UPEERS, MC_DPEERS, MC_SEED, MC_LEECH, MC_DONE, MC_DOWN, MC_UP, MC_LEFT, MC_TRACKER, MC_TORRENT, MC_ID, MC_ROW_COUNT diff --git a/gtk/tr_icon.c b/gtk/tr_icon.c index c12f2b081..fb372131a 100644 --- a/gtk/tr_icon.c +++ b/gtk/tr_icon.c @@ -23,340 +23,46 @@ *****************************************************************************/ #include -#include - +#include "actions.h" #include "tr_icon.h" #include "util.h" -#define ITEM_ACTION "tr-icon-item-action" +#ifndef STATUS_ICON_SUPPORTED -enum -{ - PROP_ICON = 1, - PROP_DOCKED, - PROP_CLICK, -}; - -static void -tr_icon_init( GTypeInstance * instance, gpointer g_class ); -static void -tr_icon_set_property( GObject * object, guint property_id, - const GValue * value, GParamSpec * pspec ); -static void -tr_icon_get_property( GObject * object, guint property_id, - GValue * value, GParamSpec * pspec); -static void -tr_icon_class_init( gpointer g_class, gpointer g_class_data ); -static void -tr_icon_dispose( GObject * obj ); -static void -itemclick( GObject * obj, gpointer data ); -static void -emitaction( TrIcon * self, int id ); -#ifdef TR_ICON_SUPPORTED -static void -clicked( TrIcon * self, gpointer data ); -static void -popup( TrIcon * self, guint button, guint when, gpointer data ); -#endif - -GType -tr_icon_get_type( void ) -{ - static GType type = 0; - - if( 0 == type ) - { - static const GTypeInfo info = - { - sizeof( TrIconClass ), - NULL, /* base_init */ - NULL, /* base_finalize */ - tr_icon_class_init, /* class_init */ - NULL, /* class_finalize */ - NULL, /* class_data */ - sizeof( TrIcon ), - 0, /* n_preallocs */ - tr_icon_init, /* instance_init */ - NULL, - }; -#ifdef TR_ICON_SUPPORTED - type = GTK_TYPE_STATUS_ICON; -#else - type = G_TYPE_OBJECT; -#endif - type = g_type_register_static( type, "TrIcon", &info, 0 ); - } - - return type; -} - -static void -tr_icon_class_init( gpointer g_class, gpointer g_class_data SHUTUP ) -{ - GObjectClass * gobject_class; - TrIconClass * tricon_class; - GParamSpec * pspec; - - gobject_class = G_OBJECT_CLASS( g_class ); - gobject_class->set_property = tr_icon_set_property; - gobject_class->get_property = tr_icon_get_property; - gobject_class->dispose = tr_icon_dispose; - - pspec = g_param_spec_boolean( "icon", "Icon", - "Icon has been set from default window icon.", - TRUE, G_PARAM_CONSTRUCT|G_PARAM_READWRITE ); - g_object_class_install_property( gobject_class, PROP_ICON, pspec ); - - pspec = g_param_spec_boolean( "docked", "Docked", - "Icon is docked in a system tray.", - FALSE, G_PARAM_READABLE ); - g_object_class_install_property( gobject_class, PROP_DOCKED, pspec ); - - pspec = g_param_spec_int( "activate-action", "Activate action", - "The action id to signal when icon is activated.", - G_MININT, G_MAXINT, -1, G_PARAM_READWRITE ); - g_object_class_install_property( gobject_class, PROP_CLICK, pspec ); - - tricon_class = TR_ICON_CLASS( g_class ); - tricon_class->actionsig = - g_signal_new( "action", G_TYPE_FROM_CLASS( g_class ), - G_SIGNAL_RUN_LAST, 0, NULL, NULL, - g_cclosure_marshal_VOID__INT, - G_TYPE_NONE, 1, G_TYPE_INT ); -} - -static void -tr_icon_init( GTypeInstance * instance, gpointer g_class SHUTUP ) -{ - TrIcon * self = ( TrIcon * )instance; - - self->clickact = -1; - self->menu = NULL; - self->actions = NULL; - self->disposed = FALSE; - -#ifdef TR_ICON_SUPPORTED - self->menu = gtk_menu_new(); - gtk_widget_show( self->menu ); - g_signal_connect( self, "activate", G_CALLBACK( clicked ), NULL ); - g_signal_connect( self, "popup-menu", G_CALLBACK( popup ), NULL ); -#endif -} - -static void -tr_icon_set_property( GObject * object, guint property_id, - const GValue * value, GParamSpec * pspec) -{ - TrIcon * self = ( TrIcon * )object; - - if( self->disposed ) - { - return; - } - - switch( property_id ) - { - case PROP_ICON: -#ifdef TR_ICON_SUPPORTED - if( g_value_get_boolean( value ) ) - { - GList * icons = gtk_window_get_default_icon_list(); - if( NULL != icons && NULL != icons->data ) - { - gtk_status_icon_set_from_pixbuf( GTK_STATUS_ICON( self ), - icons->data ); - } - g_list_free( icons ); - } - else - { - gtk_status_icon_set_from_pixbuf( GTK_STATUS_ICON( self ), - NULL ); - } -#endif - break; - case PROP_CLICK: - self->clickact = g_value_get_int( value ); - break; - case PROP_DOCKED: - default: - G_OBJECT_WARN_INVALID_PROPERTY_ID( object, property_id, pspec ); - break; - } -} - -static void -tr_icon_get_property( GObject * object, guint property_id, - GValue * value, GParamSpec * pspec ) -{ - TrIcon * self = ( TrIcon * )object; -#ifdef TR_ICON_SUPPORTED - GtkStatusIcon * icon; -#endif - - if( self->disposed ) - { - return; - } - - switch( property_id ) - { - case PROP_ICON: -#ifdef TR_ICON_SUPPORTED - icon = GTK_STATUS_ICON( self ); - if( GTK_IMAGE_PIXBUF == gtk_status_icon_get_storage_type( icon ) && - NULL != gtk_status_icon_get_pixbuf( icon ) ) - { - g_value_set_boolean( value, TRUE ); - } - else -#endif - { - g_value_set_boolean( value, FALSE ); - } - break; - case PROP_DOCKED: -#ifdef TR_ICON_SUPPORTED - if( gtk_status_icon_is_embedded( GTK_STATUS_ICON( self ) ) ) - { - g_value_set_boolean( value, TRUE ); - } - else -#endif - { - g_value_set_boolean( value, FALSE ); - } - break; - case PROP_CLICK: - g_value_set_int( value, self->clickact ); - break; - default: - G_OBJECT_WARN_INVALID_PROPERTY_ID( object, property_id, pspec ); - break; - } -} - -static void -tr_icon_dispose( GObject * obj ) -{ - TrIcon * self = ( TrIcon * )obj; - GObjectClass * parent; - - if( self->disposed ) - { - return; - } - self->disposed = TRUE; - - g_list_foreach( self->actions, ( GFunc )action_free, NULL ); - g_list_free( self->actions ); - - /* Chain up to the parent class */ - parent = g_type_class_peek( g_type_parent( TR_ICON_TYPE ) ); - parent->dispose( obj ); -} - -TrIcon * +gpointer tr_icon_new( void ) { - return g_object_new( TR_ICON_TYPE, NULL ); + return NULL; } -gboolean -tr_icon_docked( TrIcon * self ) +#else + +static void +activated ( GtkStatusIcon * self UNUSED, + gpointer user_data UNUSED ) { - gboolean ret; + action_activate ("toggle-main-window"); +} - g_object_get( self, "docked", &ret, NULL ); +static void +popup ( GtkStatusIcon * self, + guint button, + guint when, + gpointer data UNUSED ) +{ + GtkWidget * w = action_get_widget( "/icon-popup" ); + gtk_menu_popup (GTK_MENU(w), NULL, NULL, + gtk_status_icon_position_menu, + self, button, when ); +} +gpointer +tr_icon_new( void ) +{ + GtkStatusIcon * ret = gtk_status_icon_new_from_stock ("ICON_TRANSMISSION"); + g_signal_connect( ret, "activate", G_CALLBACK( activated ), NULL ); + g_signal_connect( ret, "popup-menu", G_CALLBACK( popup ), NULL ); return ret; } -void -tr_icon_action_add( TrIcon * self, int id, int flags, const char * name, - const char * icon ) -{ - struct action * act; - GtkWidget * item; - - TR_IS_ICON( self ); - if( self->disposed ) - { - return; - } - - act = action_new( id, flags, name, icon ); - - if( NULL != self->menu ) - { - if( ACTF_MENU & flags && ACTF_ALWAYS & flags ) - { - item = action_makemenu( act, ITEM_ACTION, NULL, NULL, 0, - G_CALLBACK( itemclick ), self ); - gtk_menu_shell_append( GTK_MENU_SHELL( self->menu ), item ); - act->menu = item; - } - else if( ACTF_SEPARATOR & flags ) - { - item = gtk_separator_menu_item_new(); - gtk_widget_show( item ); - gtk_menu_shell_append( GTK_MENU_SHELL( self->menu ), item ); - } - } - - self->actions = g_list_append( self->actions, act ); -} - -static void -itemclick( GObject * obj, gpointer data ) -{ - TrIcon * self; - struct action * act; - - TR_IS_ICON( data ); - self = TR_ICON( data ); - act = g_object_get_data( obj, ITEM_ACTION ); - - emitaction( self, act->id ); -} - -static void -emitaction( TrIcon * self, int id ) -{ - TrIconClass * class; - - class = g_type_class_peek( TR_ICON_TYPE ); - g_signal_emit( self, class->actionsig, 0, id ); -} - -#ifdef TR_ICON_SUPPORTED - -static void -clicked( TrIcon * self, gpointer data SHUTUP ) -{ - TR_IS_ICON( self ); - - if( self->disposed || 0 > self->clickact ) - { - return; - } - - emitaction( self, self->clickact ); -} - -static void -popup( TrIcon * self, guint button, guint when, gpointer data SHUTUP ) -{ - TR_IS_ICON( self ); - - if( self->disposed ) - { - return; - } - - gtk_menu_popup( GTK_MENU( self->menu ), NULL, NULL, - gtk_status_icon_position_menu, self, button, when ); -} - -#endif /* TR_ICON_SUPPORTED */ +#endif diff --git a/gtk/tr_icon.h b/gtk/tr_icon.h index 776930332..263f9ceb6 100644 --- a/gtk/tr_icon.h +++ b/gtk/tr_icon.h @@ -27,69 +27,13 @@ #include -#if GTK_MAJOR_VERSION > 2 || \ - ( GTK_MAJOR_VERSION == 2 && GTK_MINOR_VERSION >= 10 ) -#define TR_ICON_SUPPORTED -#define tr_icon_supported() (TRUE) +#if GTK_CHECK_VERSION(2,10,0) +#define STATUS_ICON_SUPPORTED +#define status_icon_supported() (TRUE) #else -#define tr_icon_supported() (FALSE) +#define status_icon_supported() (FALSE) #endif -#define TR_ICON_TYPE ( tr_icon_get_type() ) - -#define TR_ICON( obj ) \ - ( G_TYPE_CHECK_INSTANCE_CAST( (obj), TR_ICON_TYPE, TrIcon ) ) - -#define TR_ICON_CLASS( class ) \ - ( G_TYPE_CHECK_CLASS_CAST( (class), TR_ICON_TYPE, TrIconClass ) ) - -#define TR_IS_ICON( obj ) \ - ( G_TYPE_CHECK_INSTANCE_TYPE( (obj), TR_ICON_TYPE ) ) - -#define TR_IS_ICON_CLASS( class ) \ - ( G_TYPE_CHECK_CLASS_TYPE( (class), TR_ICON_TYPE ) ) - -#define TR_ICON_GET_CLASS( obj ) \ - ( G_TYPE_INSTANCE_GET_CLASS( (obj), TR_ICON_TYPE, TrIconClass ) ) - -typedef struct _TrIcon TrIcon; -typedef struct _TrIconClass TrIconClass; - -/* treat the contents of this structure as private */ -struct _TrIcon -{ -#ifdef TR_ICON_SUPPORTED - GtkStatusIcon parent; -#else - GObject parent; -#endif - GtkWidget * menu; - GList * actions; - int clickact; - gboolean disposed; -}; - -struct _TrIconClass -{ -#ifdef TR_ICON_SUPPORTED - GtkStatusIconClass parent; -#else - GObjectClass parent; -#endif - int actionsig; -}; - -GType -tr_icon_get_type( void ); - -TrIcon * -tr_icon_new( void ); - -gboolean -tr_icon_docked( TrIcon * icon ); - -void -tr_icon_action_add( TrIcon * icon, int id, int flags, const char * label, - const char * stock ); +gpointer tr_icon_new( void ); #endif diff --git a/gtk/tr_prefs.c b/gtk/tr_prefs.c index 6b47fcfbe..f409d6e7e 100644 --- a/gtk/tr_prefs.c +++ b/gtk/tr_prefs.c @@ -127,7 +127,7 @@ defs[] = /* PREF_ID_ICON */ { "use-tray-icon", G_TYPE_BOOLEAN, - ( tr_icon_supported() ? PR_ENABLED : PR_DISABLED ), NULL, + ( status_icon_supported() ? PR_ENABLED : PR_DISABLED ), NULL, N_("Display an _icon in the system tray"), N_("Use a system tray / dock / notification area icon") }, diff --git a/gtk/tr_torrent.c b/gtk/tr_torrent.c index ef2b47557..ffc9b3eaa 100644 --- a/gtk/tr_torrent.c +++ b/gtk/tr_torrent.c @@ -647,7 +647,7 @@ tr_torrent_status_str ( TrTorrent * gtor ) const int tpeers = MAX (st->peersTotal, 0); const int upeers = MAX (st->peersUploading, 0); const int eta = st->eta; - const double prog = st->progress * 100.0; /* [0...100] */ + const double prog = st->percentDone * 100.0; /* [0...100] */ const int status = st->status; if( TR_STATUS_CHECK_WAIT & status ) @@ -672,13 +672,20 @@ tr_torrent_status_str ( TrTorrent * gtor ) g_free(timestr); } } - else if(TR_STATUS_SEED & status) + else if( TR_STATUS_SEED & status ) { top = g_strdup_printf( ngettext( "Seeding, uploading to %d of %d peer", "Seeding, uploading to %d of %d peers", tpeers ), upeers, tpeers ); } + else if( TR_STATUS_DONE & status ) + { + top = g_strdup_printf( + ngettext( "Uploading to %d of %d peer", + "Uploading to %d of %d peers", tpeers ), + upeers, tpeers ); + } else if( TR_STATUS_STOPPING & status ) { top = g_strdup( _("Stopping...") ); diff --git a/gtk/tr_window.c b/gtk/tr_window.c index 3ce54107b..4a0025cc6 100644 --- a/gtk/tr_window.c +++ b/gtk/tr_window.c @@ -29,408 +29,35 @@ #include "transmission.h" +#include "actions.h" +#include "hig.h" #include "tr_cell_renderer_progress.h" #include "tr_core.h" #include "tr_torrent.h" #include "tr_window.h" #include "util.h" -#define ITEM_ACTION "tr-window-item-action" - -enum +typedef struct { - PROP_MODEL = 1, - PROP_SELECTION, - PROP_DOUBLECLICK, - PROP_DRAG, -}; + GtkWidget * scroll; + GtkWidget * view; + GtkWidget * status; + GtkTreeSelection * selection; + GtkCellRenderer * namerend; +} +PrivateData; -static void -tr_window_init( GTypeInstance * instance, gpointer g_class ); -static void -tr_window_set_property( GObject * object, guint property_id, - const GValue * value, GParamSpec * pspec ); -static void -tr_window_get_property( GObject * object, guint property_id, - GValue * value, GParamSpec * pspec); -static void -tr_window_class_init( gpointer g_class, gpointer g_class_data ); -static void -tr_window_dispose( GObject * obj ); -static GtkTreeView * -makeview( TrWindow * self ); -static void -stylekludge( GObject * obj, GParamSpec * spec, gpointer data ); -static void -fixbuttons( GtkTreeSelection *sel, TrWindow * self ); -static void -formatname( GtkTreeViewColumn * col, GtkCellRenderer * rend, - GtkTreeModel * model, GtkTreeIter * iter, gpointer data ); -static void -formatprog( GtkTreeViewColumn * col, GtkCellRenderer * rend, - GtkTreeModel * model, GtkTreeIter * iter, gpointer data ); -static gboolean -listclick( GtkWidget * view, GdkEventButton * event, gpointer data ); -static gboolean -listpopup( GtkWidget * view SHUTUP, gpointer data ); -static void -popupmenu( TrWindow * self, GdkEventButton * event ); -static void -itemclick( GObject * obj, gpointer data ); -static void -doubleclick( GtkWidget * view, GtkTreePath * path, - GtkTreeViewColumn * col SHUTUP, gpointer data ); -static void -emitaction( TrWindow * self, int id ); -static void -orstatus( GtkTreeModel * model, GtkTreePath * path SHUTUP, GtkTreeIter * iter, - gpointer data ); -static void -istorsel( GtkTreeModel * model, GtkTreePath * path SHUTUP, GtkTreeIter * iter, - gpointer data ); +#define PRIVATE_DATA_KEY "private-data" -GType -tr_window_get_type( void ) +PrivateData* +get_private_data( TrWindow * w ) { - static GType type = 0; - - if( 0 == type ) - { - static const GTypeInfo info = - { - sizeof( TrWindowClass ), - NULL, /* base_init */ - NULL, /* base_finalize */ - tr_window_class_init, /* class_init */ - NULL, /* class_finalize */ - NULL, /* class_data */ - sizeof( TrWindow ), - 0, /* n_preallocs */ - tr_window_init, /* instance_init */ - NULL, - }; - type = g_type_register_static( GTK_TYPE_WINDOW, "TrWindow", &info, 0 ); - } - - return type; + return (PrivateData*) g_object_get_data (G_OBJECT(w), PRIVATE_DATA_KEY); } -static void -tr_window_class_init( gpointer g_class, gpointer g_class_data SHUTUP ) -{ - GObjectClass * gobject_class; - TrWindowClass * trwindow_class; - GParamSpec * pspec; - - gobject_class = G_OBJECT_CLASS( g_class ); - gobject_class->set_property = tr_window_set_property; - gobject_class->get_property = tr_window_get_property; - gobject_class->dispose = tr_window_dispose; - - pspec = g_param_spec_object( "model", "Model", - "The GtkTreeModel for the list view.", - GTK_TYPE_TREE_MODEL, G_PARAM_READWRITE ); - g_object_class_install_property( gobject_class, PROP_MODEL, pspec ); - - pspec = g_param_spec_object( "selection", "Selection", - "The GtkTreeSelection for the list view.", - GTK_TYPE_TREE_SELECTION, G_PARAM_READABLE ); - g_object_class_install_property( gobject_class, PROP_SELECTION, pspec ); - - pspec = g_param_spec_int( "double-click-action", "Double-click action", - "The action id to signal on a double click.", - G_MININT, G_MAXINT, -1, G_PARAM_READWRITE ); - g_object_class_install_property( gobject_class, PROP_DOUBLECLICK, pspec ); - - pspec = g_param_spec_object( "drag-widget", "Drag widget", - "The GtkWidget used for drag-and-drop.", - GTK_TYPE_WIDGET, G_PARAM_READABLE ); - g_object_class_install_property( gobject_class, PROP_DRAG, pspec ); - - trwindow_class = TR_WINDOW_CLASS( g_class ); - trwindow_class->actionsig = - g_signal_new( "action", G_TYPE_FROM_CLASS( g_class ), - G_SIGNAL_RUN_LAST, 0, NULL, NULL, - g_cclosure_marshal_VOID__INT, - G_TYPE_NONE, 1, G_TYPE_INT ); -} - -static void -tr_window_init( GTypeInstance * instance, gpointer g_class SHUTUP ) -{ - TrWindow * self = ( TrWindow * )instance; - GtkWidget * vbox, * scroll, * status, * tools, * menu, * file, * item; - - vbox = gtk_vbox_new( FALSE, 0 ); - scroll = gtk_scrolled_window_new( NULL, NULL ); - status = gtk_statusbar_new(); - tools = gtk_toolbar_new(); - menu = gtk_menu_bar_new(); - file = gtk_menu_new(); - item = gtk_menu_item_new_with_mnemonic( _("_File") ); - - self->scroll = GTK_SCROLLED_WINDOW( scroll ); - self->view = makeview( self ); - self->status = GTK_STATUSBAR( status ); - self->toolbar = GTK_TOOLBAR( tools ); - self->menu = GTK_MENU_SHELL( file ); - /* this should have been set by makeview() */ - g_assert( NULL != self->namerend ); - self->doubleclick = -1; - self->actions = NULL; - self->accel = gtk_accel_group_new(); - self->stupidpopuphack = NULL; - self->disposed = FALSE; - - gtk_window_add_accel_group( GTK_WINDOW( self ), self->accel ); - gtk_menu_set_accel_group( GTK_MENU( self->menu ), self->accel ); - - gtk_menu_item_set_submenu( GTK_MENU_ITEM( item ), file ); - gtk_menu_shell_append( GTK_MENU_SHELL( menu ), item ); - gtk_box_pack_start( GTK_BOX( vbox ), menu, FALSE, FALSE, 0 ); - - gtk_toolbar_set_tooltips( self->toolbar, TRUE ); - gtk_toolbar_set_show_arrow( self->toolbar, FALSE ); - gtk_box_pack_start( GTK_BOX( vbox ), GTK_WIDGET( self->toolbar ), - FALSE, FALSE, 0 ); - - gtk_container_add( GTK_CONTAINER( scroll ), GTK_WIDGET( self->view ) ); - gtk_box_pack_start( GTK_BOX( vbox ), scroll, TRUE, TRUE, 0 ); - - gtk_statusbar_push( self->status, 0, "" ); - gtk_box_pack_start( GTK_BOX( vbox ), GTK_WIDGET( self->status ), - FALSE, FALSE, 0 ); - - gtk_container_set_focus_child( GTK_CONTAINER( vbox ), scroll ); - gtk_widget_show_all( vbox ); - gtk_container_add( GTK_CONTAINER( self ), vbox ); - gtk_window_set_title( GTK_WINDOW( self ), g_get_application_name()); - gtk_window_set_role( GTK_WINDOW( self ), "tr-main" ); -} - -static void -tr_window_set_property( GObject * object, guint property_id, - const GValue * value SHUTUP, GParamSpec * pspec) -{ - TrWindow * self = ( TrWindow * )object; - - if( self->disposed ) - { - return; - } - - switch( property_id ) - { - case PROP_MODEL: - gtk_tree_view_set_model( self->view, g_value_get_object( value ) ); - break; - case PROP_DOUBLECLICK: - self->doubleclick = g_value_get_int( value ); - break; - default: - G_OBJECT_WARN_INVALID_PROPERTY_ID( object, property_id, pspec ); - break; - } -} - -static void -tr_window_get_property( GObject * object, guint property_id, - GValue * value SHUTUP, GParamSpec * pspec ) -{ - TrWindow * self = ( TrWindow * )object; - - if( self->disposed ) - { - return; - } - - switch( property_id ) - { - case PROP_MODEL: - g_value_set_object( value, gtk_tree_view_get_model( self->view ) ); - break; - case PROP_SELECTION: - g_value_set_object( value, - gtk_tree_view_get_selection( self->view ) ); - break; - case PROP_DOUBLECLICK: - g_value_set_int( value, self->doubleclick ); - break; - case PROP_DRAG: - g_value_set_object( value, self->view ); - break; - default: - G_OBJECT_WARN_INVALID_PROPERTY_ID( object, property_id, pspec ); - break; - } -} - -static void -tr_window_dispose( GObject * obj ) -{ - TrWindow * self = ( TrWindow * )obj; - GObjectClass * parent; - - if( self->disposed ) - { - return; - } - self->disposed = TRUE; - - g_list_foreach( self->actions, ( GFunc )action_free, NULL ); - g_list_free( self->actions ); - g_object_unref( self->accel ); - if( NULL != self->stupidpopuphack ) - { - gtk_widget_destroy( self->stupidpopuphack ); - } - self->stupidpopuphack = NULL; - - /* Chain up to the parent class */ - parent = g_type_class_peek( g_type_parent( TR_WINDOW_TYPE ) ); - parent->dispose( obj ); -} - -GtkWidget * -tr_window_new( void ) -{ - return g_object_new( TR_WINDOW_TYPE, NULL ); -} - -void -tr_window_action_add( TrWindow * self, int id, int flags, const char * name, - const char * icon, const char * description, guint key ) -{ - struct action * act; - GtkWidget * sep; - - TR_IS_WINDOW( self ); - if( self->disposed ) - { - return; - } - - act = action_new( id, flags, name, icon ); - - if( ACTF_TOOL & flags ) - { - act->tool = action_maketool( act, ITEM_ACTION, - G_CALLBACK( itemclick ), self ); - gtk_tool_item_set_tooltip( GTK_TOOL_ITEM( act->tool ), - self->toolbar->tooltips, description, "" ); - gtk_toolbar_insert( self->toolbar, GTK_TOOL_ITEM( act->tool ), -1 ); - } - - if( ACTF_MENU & flags ) - { - act->menu = action_makemenu( act, ITEM_ACTION, self->accel, - "/file", key, - G_CALLBACK( itemclick ), self ); - gtk_menu_shell_append( self->menu, act->menu ); - } - - if( ACTF_SEPARATOR & flags ) - { - sep = gtk_separator_menu_item_new(); - gtk_widget_show( sep ); - gtk_menu_shell_append( self->menu, sep ); - } - - self->actions = g_list_append( self->actions, act ); -} - -void -tr_window_update( TrWindow * self, float downspeed, float upspeed ) -{ - char * downstr, * upstr, * str; - - TR_IS_WINDOW( self ); - if( self->disposed ) - { - return; - } - - /* update the status bar */ - downstr = readablespeed ( downspeed ); - upstr = readablespeed ( upspeed ); - str = g_strdup_printf( _(" Total DL: %s Total UL: %s"), - downstr, upstr ); - g_free( downstr ); - g_free( upstr ); - gtk_statusbar_pop( self->status, 0 ); - gtk_statusbar_push( self->status, 0, str ); - g_free( str ); - - /* the selection's status may have changed so update the buttons */ - fixbuttons( NULL, self ); -} - -void -tr_window_show( TrWindow * self ) -{ - TR_IS_WINDOW( self ); - - sizingmagic( GTK_WINDOW( self ), self->scroll, - GTK_POLICY_AUTOMATIC, GTK_POLICY_ALWAYS ); - g_object_set( self->namerend, "ellipsize", PANGO_ELLIPSIZE_END, NULL ); - gtk_widget_show( GTK_WIDGET( self ) ); -} - -static GtkTreeView * -makeview( TrWindow * self ) -{ - GtkWidget * view; - GtkTreeViewColumn * col; - GtkTreeSelection * sel; - GtkCellRenderer * namerend, * progrend; - char * str; - - TR_IS_WINDOW( self ); - - view = gtk_tree_view_new(); - namerend = gtk_cell_renderer_text_new(); - self->namerend = G_OBJECT( namerend ); - /* note that this renderer is set to ellipsize, just not here */ - col = gtk_tree_view_column_new_with_attributes( _("Name"), namerend, - NULL ); - gtk_tree_view_column_set_cell_data_func( col, namerend, formatname, - NULL, NULL ); - gtk_tree_view_column_set_expand( col, TRUE ); - gtk_tree_view_column_set_sizing( col, GTK_TREE_VIEW_COLUMN_AUTOSIZE ); - gtk_tree_view_append_column( GTK_TREE_VIEW( view ), col ); - - progrend = tr_cell_renderer_progress_new(); - /* this string is only used to determine the size of the progress bar */ - str = g_markup_printf_escaped( "%s", _(" fnord fnord ") ); - g_object_set( progrend, "bar-sizing", str, NULL ); - g_free(str); - col = gtk_tree_view_column_new_with_attributes( _("Progress"), progrend, - NULL); - gtk_tree_view_column_set_cell_data_func( col, progrend, formatprog, - NULL, NULL ); - gtk_tree_view_column_set_sizing( col, GTK_TREE_VIEW_COLUMN_AUTOSIZE ); - gtk_tree_view_append_column( GTK_TREE_VIEW( view ), col ); - - /* XXX this shouldn't be necessary */ - g_signal_connect( view, "notify::style", - G_CALLBACK( stylekludge ), progrend ); - - gtk_tree_view_set_rules_hint( GTK_TREE_VIEW( view ), TRUE ); - sel = gtk_tree_view_get_selection( GTK_TREE_VIEW( view ) ); - gtk_tree_selection_set_mode( GTK_TREE_SELECTION( sel ), - GTK_SELECTION_MULTIPLE ); - - g_signal_connect( G_OBJECT( sel ), "changed", - G_CALLBACK( fixbuttons ), self ); - g_signal_connect( G_OBJECT( view ), "button-press-event", - G_CALLBACK( listclick ), self ); - g_signal_connect( G_OBJECT( view ), "popup-menu", - G_CALLBACK( listpopup ), self ); - g_signal_connect( G_OBJECT( view ), "row-activated", - G_CALLBACK( doubleclick ), self ); - - return GTK_TREE_VIEW( view ); -} +/*** +**** +***/ /* kludge to have the progress bars notice theme changes */ static void @@ -444,49 +71,6 @@ stylekludge( GObject * obj, GParamSpec * spec, gpointer data ) } } -/* disable buttons and menuitems the user shouldn't be able to click on */ -static void -fixbuttons( GtkTreeSelection *sel, TrWindow * self ) { - gboolean selected, avail; - GList * ii; - int status; - struct action * act; - - TR_IS_WINDOW( self ); - if( self->disposed ) - { - return; - } - - if( NULL == sel ) - { - sel = gtk_tree_view_get_selection( self->view ); - } - status = 0; - gtk_tree_selection_selected_foreach( sel, orstatus, &status ); - selected = ( 0 < gtk_tree_selection_count_selected_rows( sel ) ); - - for( ii = g_list_first( self->actions ); NULL != ii; ii = ii->next ) - { - act = ii->data; - if( ACTF_ALWAYS & act->flags ) - { - continue; - } - avail = ACT_ISAVAIL( act->flags, status ); - if( ACTF_TOOL & act->flags ) - { - g_assert( NULL != act->tool ); - gtk_widget_set_sensitive( act->tool, selected && avail ); - } - if( ACTF_MENU & act->flags ) - { - g_assert( NULL != act->menu ); - gtk_widget_set_sensitive( act->menu, selected && avail ); - } - } -} - static void formatname( GtkTreeViewColumn * col SHUTUP, GtkCellRenderer * rend, GtkTreeModel * model, GtkTreeIter * iter, gpointer data SHUTUP ) @@ -546,7 +130,7 @@ formatprog( GtkTreeViewColumn * col SHUTUP, GtkCellRenderer * rend, gfloat prog, dl, ul; guint64 down, up; - gtk_tree_model_get( model, iter, MC_PROG, &prog, MC_DRATE, &dl, + gtk_tree_model_get( model, iter, MC_PROG_D, &prog, MC_DRATE, &dl, MC_URATE, &ul, MC_DOWN, &down, MC_UP, &up, -1 ); prog = MAX( prog, 0.0 ); prog = MIN( prog, 1.0 ); @@ -570,199 +154,156 @@ formatprog( GtkTreeViewColumn * col SHUTUP, GtkCellRenderer * rend, g_free( marked ); } -/* show a popup menu for a right-click on the list */ -static gboolean -listclick( GtkWidget * view, GdkEventButton * event, gpointer data ) +static void +on_popup_menu ( GtkWidget * self UNUSED, GdkEventButton * event ) { - TrWindow * self; - GtkTreeSelection * sel; - GtkTreePath * path; - GtkTreeModel * model; - GtkTreeIter iter; - int status; - TrTorrent * tor, * issel; + GtkWidget * menu = action_get_widget ( "/main-window-popup" ); + gtk_menu_popup (GTK_MENU(menu), NULL, NULL, NULL, NULL, + (event ? event->button : 0), + (event ? event->time : 0)); +} - if( GDK_BUTTON_PRESS != event->type || 3 != event->button ) - { - return FALSE; - } +static void +view_row_activated ( GtkTreeView * tree_view UNUSED, + GtkTreePath * path UNUSED, + GtkTreeViewColumn * column UNUSED, + gpointer user_data UNUSED ) +{ + action_activate( "show-torrent-inspector" ); +} - TR_IS_WINDOW( data ); - self = TR_WINDOW( data ); - if( self->disposed ) - { - return FALSE; - } +static GtkWidget* +makeview( PrivateData * p ) +{ + GtkWidget * view; + GtkTreeViewColumn * col; + GtkTreeSelection * sel; + GtkCellRenderer * namerend, * progrend; + char * str; + view = gtk_tree_view_new(); + + p->selection = gtk_tree_view_get_selection( GTK_TREE_VIEW(view) ); + namerend = gtk_cell_renderer_text_new(); + p->namerend = namerend; + /* note that this renderer is set to ellipsize, just not here */ + col = gtk_tree_view_column_new_with_attributes( _("Name"), namerend, + NULL ); + gtk_tree_view_column_set_cell_data_func( col, namerend, formatname, + NULL, NULL ); + gtk_tree_view_column_set_expand( col, TRUE ); + gtk_tree_view_column_set_sizing( col, GTK_TREE_VIEW_COLUMN_AUTOSIZE ); + gtk_tree_view_append_column( GTK_TREE_VIEW( view ), col ); + + progrend = tr_cell_renderer_progress_new(); + /* this string is only used to determine the size of the progress bar */ + str = g_markup_printf_escaped( "%s", _(" fnord fnord ") ); + g_object_set( progrend, "bar-sizing", str, NULL ); + g_free(str); + col = gtk_tree_view_column_new_with_attributes( _("Progress"), progrend, + NULL); + gtk_tree_view_column_set_cell_data_func( col, progrend, formatprog, + NULL, NULL ); + gtk_tree_view_column_set_sizing( col, GTK_TREE_VIEW_COLUMN_AUTOSIZE ); + gtk_tree_view_append_column( GTK_TREE_VIEW( view ), col ); + + /* XXX this shouldn't be necessary */ + g_signal_connect( view, "notify::style", + G_CALLBACK( stylekludge ), progrend ); + + gtk_tree_view_set_rules_hint( GTK_TREE_VIEW( view ), TRUE ); sel = gtk_tree_view_get_selection( GTK_TREE_VIEW( view ) ); - model = gtk_tree_view_get_model( GTK_TREE_VIEW( view ) ); + gtk_tree_selection_set_mode( GTK_TREE_SELECTION( sel ), + GTK_SELECTION_MULTIPLE ); - /* find what row, if any, the user clicked on */ - if( gtk_tree_view_get_path_at_pos( GTK_TREE_VIEW( view ), - event->x, event->y, &path, - NULL, NULL, NULL ) ) - { - if( gtk_tree_model_get_iter( model, &iter, path ) ) - { - /* get torrent and status for the right-clicked row */ - gtk_tree_model_get( model, &iter, MC_TORRENT, &tor, - MC_STAT, &status, -1 ); - issel = tor; - gtk_tree_selection_selected_foreach( sel, istorsel, &issel ); - g_object_unref( tor ); - /* if the clicked row isn't selected, select only it */ - if( NULL != issel ) - { - gtk_tree_selection_unselect_all( sel ); - gtk_tree_selection_select_iter( sel, &iter ); - } - } - gtk_tree_path_free( path ); - } - else - { - gtk_tree_selection_unselect_all( sel ); - } + g_signal_connect( view, "popup-menu", + G_CALLBACK(on_popup_menu), NULL ); + g_signal_connect( view, "button-press-event", + G_CALLBACK(on_tree_view_button_pressed), on_popup_menu); + g_signal_connect( view, "row-activated", + G_CALLBACK(view_row_activated), NULL); - popupmenu( self, event ); - - return TRUE; -} - -static gboolean -listpopup( GtkWidget * view SHUTUP, gpointer data ) -{ - popupmenu( TR_WINDOW( data ), NULL ); - return TRUE; + return view; } static void -popupmenu( TrWindow * self, GdkEventButton * event ) +realized_cb ( GtkWidget * wind, gpointer unused UNUSED ) { - GtkTreeSelection * sel; - int count, status; - GtkWidget * menu, * item; - GList * ii; - struct action * act; - - TR_IS_WINDOW( self ); - if( self->disposed ) - { - return; - } - - sel = gtk_tree_view_get_selection( self->view ); - count = gtk_tree_selection_count_selected_rows( sel ); - menu = gtk_menu_new(); - - if( NULL != self->stupidpopuphack ) - { - gtk_widget_destroy( self->stupidpopuphack ); - } - self->stupidpopuphack = menu; - - status = 0; - gtk_tree_selection_selected_foreach( sel, orstatus, &status ); - - for( ii = g_list_first( self->actions ); NULL != ii; ii = ii->next ) - { - act = ii->data; - if( ACTF_SEPARATOR & act->flags ) - { - item = gtk_separator_menu_item_new(); - gtk_widget_show( item ); - gtk_menu_shell_append( GTK_MENU_SHELL( menu ), item ); - } - else if( ACTF_MENU & act->flags && ACT_ISAVAIL( act->flags, status ) ) - { - item = action_makemenu( act, ITEM_ACTION, NULL, NULL, 0, - G_CALLBACK( itemclick ), self ); - gtk_menu_shell_append( GTK_MENU_SHELL( menu ), item ); - } - } - - gtk_widget_show( menu ); - - gtk_menu_popup( GTK_MENU( menu ), NULL, NULL, NULL, NULL, - ( NULL == event ? 0 : event->button ), - gdk_event_get_time( (GdkEvent*)event ) ); + PrivateData * p = get_private_data( GTK_WINDOW( wind ) ); + sizingmagic( GTK_WINDOW(wind), GTK_SCROLLED_WINDOW( p->scroll ), + GTK_POLICY_AUTOMATIC, GTK_POLICY_ALWAYS ); + g_object_set( p->namerend, "ellipsize", PANGO_ELLIPSIZE_END, NULL ); } -static void -itemclick( GObject * obj, gpointer data ) +/*** +**** PUBLIC +***/ + +GtkWidget * +tr_window_new( GtkUIManager * ui_manager ) { - TrWindow * self; - struct action * act; + PrivateData * p = g_new( PrivateData, 1 ); + GtkWidget *vbox, *w, *self; - TR_IS_WINDOW( data ); - self = TR_WINDOW( data ); - act = g_object_get_data( obj, ITEM_ACTION ); + /* make the window */ + self = gtk_window_new (GTK_WINDOW_TOPLEVEL); + g_object_set_data_full(G_OBJECT(self), PRIVATE_DATA_KEY, p, g_free ); + gtk_window_set_title( GTK_WINDOW( self ), g_get_application_name()); + gtk_window_set_role( GTK_WINDOW( self ), "tr-main" ); + gtk_window_add_accel_group (GTK_WINDOW(self), + gtk_ui_manager_get_accel_group (ui_manager)); + g_signal_connect( self, "realize", G_CALLBACK(realized_cb), NULL); - emitaction( self, act->id ); + /* window's main container */ + vbox = gtk_vbox_new (FALSE, 0); + gtk_container_add (GTK_CONTAINER(self), vbox); + + /* main menu */ + w = action_get_widget( "/main-window-menu" ); + gtk_box_pack_start( GTK_BOX(vbox), w, FALSE, FALSE, 0 ); + + /* toolbar */ + w = action_get_widget( "/main-window-toolbar" ); + gtk_box_pack_start( GTK_BOX(vbox), w, FALSE, FALSE, 0 ); + + /* workarea */ + p->view = makeview( p ); + w = p->scroll = gtk_scrolled_window_new( NULL, NULL ); + gtk_container_add( GTK_CONTAINER(w), p->view ); + gtk_box_pack_start_defaults( GTK_BOX(vbox), w ); + gtk_container_set_focus_child( GTK_CONTAINER( vbox ), w ); + + /* statusbar */ + w = p->status = gtk_statusbar_new (); + gtk_statusbar_push( GTK_STATUSBAR(w), 0, "" ); + gtk_box_pack_start( GTK_BOX(vbox), w, FALSE, FALSE, 0 ); + + /* show all but the window */ + gtk_widget_show_all( vbox ); + + return self; } -static void -doubleclick( GtkWidget * view SHUTUP, GtkTreePath * path, - GtkTreeViewColumn * col SHUTUP, gpointer data ) +void +tr_window_update( TrWindow * self, float downspeed, float upspeed ) { - TrWindow * self; - GtkTreeSelection * sel; + PrivateData * p = get_private_data( self ); + GtkStatusbar * status = GTK_STATUSBAR( p->status ); - TR_IS_WINDOW( data ); - self = TR_WINDOW( data ); - if( self->disposed || 0 > self->doubleclick ) - { - return; - } - - sel = gtk_tree_view_get_selection( self->view ); - gtk_tree_selection_select_path( sel, path ); - - emitaction( self, self->doubleclick ); + /* update the status bar */ + char * downstr = readablespeed ( downspeed ); + char * upstr = readablespeed ( upspeed ); + char * str = g_strdup_printf( _(" Total DL: %s Total UL: %s"), + downstr, upstr ); + g_free( downstr ); + g_free( upstr ); + gtk_statusbar_pop( status, 0 ); + gtk_statusbar_push( status, 0, str ); + g_free( str ); } -static void -emitaction( TrWindow * self, int id ) +GtkTreeSelection* +tr_window_get_selection ( TrWindow * w ) { - TrWindowClass * class; - - TR_IS_WINDOW( self ); - if( self->disposed ) - { - return; - } - - class = g_type_class_peek( TR_WINDOW_TYPE ); - g_signal_emit( self, class->actionsig, 0, id ); -} - -/* use with gtk_tree_selection_selected_foreach to | status of selected rows */ -static void -orstatus( GtkTreeModel * model, GtkTreePath * path SHUTUP, GtkTreeIter * iter, - gpointer data ) -{ - int * allstatus, thisstatus; - - allstatus = data; - gtk_tree_model_get( model, iter, MC_STAT, &thisstatus, -1 ); - *allstatus |= thisstatus; -} - -/* data should be a TrTorrent**, will set torrent to NULL if it's selected */ -static void -istorsel( GtkTreeModel * model, GtkTreePath * path SHUTUP, GtkTreeIter * iter, - gpointer data ) -{ - TrTorrent ** torref, * tor; - - torref = data; - if( NULL != *torref ) - { - gtk_tree_model_get( model, iter, MC_TORRENT, &tor, -1 ); - if( tor == *torref ) - { - *torref = NULL; - } - g_object_unref( tor ); - } + return get_private_data(w)->selection; } diff --git a/gtk/tr_window.h b/gtk/tr_window.h index d066340ed..258d98065 100644 --- a/gtk/tr_window.h +++ b/gtk/tr_window.h @@ -25,67 +25,15 @@ #ifndef TR_WINDOW_H #define TR_WINDOW_H -#include #include -#define TR_WINDOW_TYPE ( tr_window_get_type() ) +typedef GtkWindow TrWindow; -#define TR_WINDOW( obj ) \ - ( G_TYPE_CHECK_INSTANCE_CAST( (obj), TR_WINDOW_TYPE, TrWindow ) ) +GtkTreeSelection * tr_window_get_selection( TrWindow* wind ); -#define TR_WINDOW_CLASS( class ) \ - ( G_TYPE_CHECK_CLASS_CAST( (class), TR_WINDOW_TYPE, TrWindowClass ) ) - -#define TR_IS_WINDOW( obj ) \ - ( G_TYPE_CHECK_INSTANCE_TYPE( (obj), TR_WINDOW_TYPE ) ) - -#define TR_IS_WINDOW_CLASS( class ) \ - ( G_TYPE_CHECK_CLASS_TYPE( (class), TR_WINDOW_TYPE ) ) - -#define TR_WINDOW_GET_CLASS( obj ) \ - ( G_TYPE_INSTANCE_GET_CLASS( (obj), TR_WINDOW_TYPE, TrWindowClass ) ) - -typedef struct _TrWindow TrWindow; -typedef struct _TrWindowClass TrWindowClass; - -/* treat the contents of this structure as private */ -struct _TrWindow -{ - GtkWindow parent; - GtkScrolledWindow * scroll; - GtkTreeView * view; - GtkStatusbar * status; - GtkToolbar * toolbar; - GtkMenuShell * menu; - GObject * namerend; - int doubleclick; - GList * actions; - GtkAccelGroup * accel; - GtkWidget * stupidpopuphack; - gboolean disposed; -}; - -struct _TrWindowClass -{ - GtkWindowClass parent; - int actionsig; -}; - -GType -tr_window_get_type( void ); - -GtkWidget * -tr_window_new( void ); - -void -tr_window_action_add( TrWindow * wind, int id, int flags, const char * name, - const char * icon, const char * description, guint key ); +GtkWidget * tr_window_new( GtkUIManager* ); void tr_window_update( TrWindow * wind, float downspeed, float upspeed ); -/* some magic to show the window with a nice initial size */ -void -tr_window_show( TrWindow * wind ); - #endif diff --git a/gtk/transmission-gtk.1 b/gtk/transmission-gtk.1 index 44b202bb2..7aed2350e 100644 --- a/gtk/transmission-gtk.1 +++ b/gtk/transmission-gtk.1 @@ -77,8 +77,9 @@ The program was written by .An Josh Elsasser Aq josh@elsasser.org , .An Eric Petit Aq titer@m0k.org , +.An Mitchell Livingston Aq livings124@gmail.com , and -.An Mitchell Livingston Aq livings124@gmail.com . +.An Charles Kerr Aq charles@rebelbase.com . .Sh SEE ALSO .Xr transmissioncli 1 , .Xr transmission-daemon 1 , diff --git a/gtk/ui.h b/gtk/ui.h new file mode 100644 index 000000000..28eae236d --- /dev/null +++ b/gtk/ui.h @@ -0,0 +1,66 @@ +const char * fallback_ui_file = +"\n" +" \n" +" \n" +" \n" +" \n" +" \n" +" \n" +" \n" +" \n" +" \n" +" \n" +" \n" +" \n" +" \n" +" \n" +" \n" +" \n" +" \n" +" \n" +" \n" +" \n" +" \n" +" \n" +" \n" +"\n" +" \n" +" \n" +" \n" +" \n" +" \n" +" \n" +" \n" +" \n" +"\n" +" \n" +" \n" +" \n" +" \n" +" \n" +" \n" +" \n" +" \n" +" \n" +"\n" +" \n" +" \n" +" \n" +" \n" +" \n" +" \n" +" \n" +" \n" +" \n" +"\n" +" \n" +" \n" +" \n" +" \n" +" \n" +" \n" +" \n" +" \n" +"\n" +""; + diff --git a/gtk/util.c b/gtk/util.c index 11fed982e..4d1b4d4b5 100644 --- a/gtk/util.c +++ b/gtk/util.c @@ -319,128 +319,38 @@ getdownloaddir( void ) return dir; } +/** + * don't use more than 50% the height of the screen, nor 80% the width. + * but don't be too small, either -- set the minimums to 500 x 300 + */ void -sizingmagic( GtkWindow * wind, GtkScrolledWindow * scroll, - GtkPolicyType hscroll, GtkPolicyType vscroll ) +sizingmagic( GtkWindow * wind, + GtkScrolledWindow * scroll, + GtkPolicyType hscroll, + GtkPolicyType vscroll ) { + int width; + int height; GtkRequisition req; - GdkScreen * screen; - int width, height; - screen = gtk_widget_get_screen( GTK_WIDGET( wind ) ); + GdkScreen * screen = gtk_widget_get_screen( GTK_WIDGET( wind ) ); gtk_scrolled_window_set_policy( scroll, GTK_POLICY_NEVER, - GTK_POLICY_NEVER ); + GTK_POLICY_NEVER ); gtk_widget_size_request( GTK_WIDGET( wind ), &req ); + req.height = MAX( req.height, 300 ); height = MIN( req.height, gdk_screen_get_height( screen ) / 5 * 4 ); gtk_scrolled_window_set_policy( scroll, GTK_POLICY_NEVER, vscroll ); - gtk_widget_size_request( GTK_WIDGET( wind ), &req ); + req.width = MAX( req.width, 500 ); width = MIN( req.width, gdk_screen_get_width( screen ) / 2 ); gtk_window_set_default_size( wind, width, height ); - gtk_scrolled_window_set_policy( scroll, hscroll, vscroll ); } -struct action * -action_new( int id, int flags, const char * label, const char * stock ) -{ - struct action * act; - - act = g_new0( struct action, 1 ); - act->id = id; - act->flags = flags; - act->label = g_strdup( label ); - act->stock = g_strdup( stock ); - act->tool = NULL; - act->menu = NULL; - - return act; -} - -void -action_free( struct action * act ) -{ - g_free( act->label ); - g_free( act->stock ); - g_free( act ); -} - -GtkWidget * -action_maketool( struct action * act, const char * key, - GCallback func, gpointer data ) -{ - GtkToolItem * item; - - item = gtk_tool_button_new_from_stock( act->stock ); - if( NULL != act->label ) - { - gtk_tool_button_set_label( GTK_TOOL_BUTTON( item ), act->label ); - } - g_object_set_data( G_OBJECT( item ), key, act ); - g_signal_connect( item, "clicked", func, data ); - gtk_widget_show( GTK_WIDGET( item ) ); - - return GTK_WIDGET( item ); -} - -GtkWidget * -action_makemenu( struct action * act, const char * actkey, - GtkAccelGroup * accel, const char * path, guint keyval, - GCallback func, gpointer data ) -{ - GtkWidget * item, * label; - GdkModifierType mod; - GtkStockItem stock; - const char * name; - char * joined; - - mod = GDK_CONTROL_MASK; - name = act->label; - if( NULL == act->stock ) - { - item = gtk_menu_item_new_with_label( act->label ); - } - else - { - item = gtk_image_menu_item_new_from_stock( act->stock, NULL ); - if( NULL == act->label ) - { - if( gtk_stock_lookup( act->stock, &stock ) ) - { - name = stock.label; - if( 0 == keyval ) - { - keyval = stock.keyval; - mod = stock.modifier; - } - } - } - else - { - label = gtk_bin_get_child( GTK_BIN( item ) ); - gtk_label_set_text( GTK_LABEL( label ), act->label ); - - } - } - - if( NULL != accel && 0 < keyval && NULL != name ) - { - joined = g_strjoin( "/", path, name, NULL ); - gtk_accel_map_add_entry( joined, keyval, mod ); - gtk_widget_set_accel_path( item, joined, accel ); - g_free( joined ); - } - g_object_set_data( G_OBJECT( item ), actkey, act ); - g_signal_connect( item, "activate", func, data ); - gtk_widget_show( item ); - - return item; -} - void errmsg( GtkWindow * wind, const char * format, ... ) { @@ -519,3 +429,39 @@ errcb(GtkWidget *widget, int resp SHUTUP, gpointer data) { gtk_widget_destroy(widget); } + +/* pop up the context menu if a user right-clicks. + if the row they right-click on isn't selected, select it. */ +gboolean +on_tree_view_button_pressed (GtkWidget * view, + GdkEventButton * event, + gpointer func) +{ + GtkTreeView * tv = GTK_TREE_VIEW( view ); + + if (event->type == GDK_BUTTON_PRESS && event->button == 3) + { + GtkTreeSelection * selection = gtk_tree_view_get_selection(tv); + GtkTreePath *path; + if (gtk_tree_view_get_path_at_pos (tv, + (gint) event->x, + (gint) event->y, + &path, NULL, NULL, NULL)) + { + if (!gtk_tree_selection_path_is_selected (selection, path)) + { + gtk_tree_selection_unselect_all (selection); + gtk_tree_selection_select_path (selection, path); + } + gtk_tree_path_free(path); + } + + typedef void (PopupFunc)(GtkWidget*, GdkEventButton*); + ((PopupFunc*)func)(view, event); + + return TRUE; + } + + return FALSE; +} + diff --git a/gtk/util.h b/gtk/util.h index e76176f2f..491bc9e68 100644 --- a/gtk/util.h +++ b/gtk/util.h @@ -46,24 +46,6 @@ enum tr_torrent_action { TR_TOR_LEAVE, TR_TOR_COPY, TR_TOR_MOVE }; /* used for a callback function with a data parameter */ typedef void (*callbackfunc_t)(void*); -/* flags indicating where and when an action is valid */ -#define ACTF_TOOL ( 1 << 0 ) /* appear in the toolbar */ -#define ACTF_MENU ( 1 << 1 ) /* appear in the popup menu */ -#define ACTF_ALWAYS ( 1 << 2 ) /* available regardless of selection */ -#define ACTF_ACTIVE ( 1 << 3 ) /* available for active torrent */ -#define ACTF_INACTIVE ( 1 << 4 ) /* available for inactive torrent */ -#define ACTF_SEPARATOR ( 1 << 5 ) /* dummy action to create menu separator */ -/* appear in the toolbar and the popup menu */ -#define ACTF_WHEREVER ( ACTF_TOOL | ACTF_MENU ) -/* available if there is something selected */ -#define ACTF_WHATEVER ( ACTF_ACTIVE | ACTF_INACTIVE ) - -/* checking action flags against torrent status */ -#define ACT_ISAVAIL( flags, status ) \ - ( ( ACTF_ACTIVE & (flags) && TR_STATUS_ACTIVE & (status) ) || \ - ( ACTF_INACTIVE & (flags) && TR_STATUS_INACTIVE & (status) ) || \ - ACTF_ALWAYS & (flags) ) - /* try to interpret a string as a textual representation of a boolean */ /* note that this isn't localized */ gboolean @@ -131,30 +113,6 @@ getdownloaddir( void ); #ifdef GTK_MAJOR_VERSION -/* action handling */ -struct action -{ - int id; - int flags; - char * label; - char * stock; - GtkWidget * tool; - GtkWidget * menu; - callbackfunc_t func; - gpointer data; -}; -struct action * -action_new( int id, int flags, const char * label, const char * stock ); -void -action_free( struct action * act ); -GtkWidget * -action_maketool( struct action * act, const char * key, - GCallback func, gpointer data ); -GtkWidget * -action_makemenu( struct action * act, const char * actkey, - GtkAccelGroup * accel, const char * path, guint keyval, - GCallback func, gpointer data ); - /* here there be dragons */ void sizingmagic( GtkWindow * wind, GtkScrolledWindow * scroll, @@ -184,6 +142,13 @@ GtkWidget * verrmsg_full( GtkWindow * wind, callbackfunc_t func, void * data, const char * format, va_list ap ); +/* pop up the context menu if a user right-clicks. + if the row they right-click on isn't selected, select it. */ +gboolean +on_tree_view_button_pressed (GtkWidget * view, + GdkEventButton * event, + gpointer unused); + #endif /* GTK_MAJOR_VERSION */ #endif /* TG_UTIL_H */ diff --git a/libtransmission/choking.c b/libtransmission/choking.c index b80bf54da..3fb1c49d5 100644 --- a/libtransmission/choking.c +++ b/libtransmission/choking.c @@ -29,19 +29,8 @@ # define lrintf(a) ((int)(0.5+(a))) #endif -/* We may try to allocate and free tables of size 0. Quick and dirty - way to handle it... */ -void * tr_malloc( size_t size ) -{ - if( !size ) - return NULL; - return malloc( size ); -} -void tr_free( void * p ) -{ - if( p ) - free( p ); -} +/* We may try to allocate and free tables of size 0. + Quick and dirty way to handle it... */ #define malloc tr_malloc #define free tr_free diff --git a/libtransmission/completion.c b/libtransmission/completion.c index 3936b6858..bf5bdf739 100644 --- a/libtransmission/completion.c +++ b/libtransmission/completion.c @@ -23,6 +23,29 @@ *****************************************************************************/ #include "transmission.h" +#include "completion.h" + +struct tr_completion_s +{ + tr_torrent_t * tor; + tr_bitfield_t * blockBitfield; + uint8_t * blockDownloaders; + tr_bitfield_t * pieceBitfield; + + /* a block is missing if we don't have it AND there's not a request pending */ + int * missingBlocks; + + /* a block is complete if and only if we have it */ + int * completeBlocks; + + /* rather than calculating these over and over again in loops, + just calculate them once */ + int nBlocksInPiece; + int nBlocksInLastPiece; +}; + +#define tr_cpCountBlocks(cp,piece) \ + (piece==cp->tor->info.pieceCount-1 ? cp->nBlocksInLastPiece : cp->nBlocksInPiece) tr_completion_t * tr_cpInit( tr_torrent_t * tor ) { @@ -34,6 +57,11 @@ tr_completion_t * tr_cpInit( tr_torrent_t * tor ) cp->blockDownloaders = malloc( tor->blockCount ); cp->pieceBitfield = tr_bitfieldNew( tor->info.pieceCount ); cp->missingBlocks = malloc( tor->info.pieceCount * sizeof( int ) ); + cp->completeBlocks = malloc( tor->info.pieceCount * sizeof( int ) ); + + cp->nBlocksInLastPiece = tr_pieceCountBlocks ( tor->info.pieceCount - 1 ); + cp->nBlocksInPiece = tor->info.pieceCount==1 ? cp->nBlocksInLastPiece + : tr_pieceCountBlocks( 0 ); tr_cpReset( cp ); @@ -46,6 +74,7 @@ void tr_cpClose( tr_completion_t * cp ) free( cp->blockDownloaders ); tr_bitfieldFree( cp->pieceBitfield ); free( cp->missingBlocks ); + free( cp->completeBlocks ); free( cp ); } @@ -54,89 +83,67 @@ void tr_cpReset( tr_completion_t * cp ) tr_torrent_t * tor = cp->tor; int i; - cp->blockCount = 0; tr_bitfieldClear( cp->blockBitfield ); memset( cp->blockDownloaders, 0, tor->blockCount ); tr_bitfieldClear( cp->pieceBitfield ); - for( i = 0; i < tor->info.pieceCount; i++ ) - { - cp->missingBlocks[i] = tr_pieceCountBlocks( i ); + for( i = 0; i < tor->info.pieceCount; ++i ) { + cp->missingBlocks[i] = tr_cpCountBlocks( cp, i ); + cp->completeBlocks[i] = 0; } } -float tr_cpCompletionAsFloat( tr_completion_t * cp ) +int tr_cpPieceHasAllBlocks( const tr_completion_t * cp, int piece ) { - return (float) cp->blockCount / (float) cp->tor->blockCount; + return tr_cpPieceIsComplete( cp, piece ); } -uint64_t tr_cpLeftBytes( tr_completion_t * cp ) +int tr_cpPieceIsComplete( const tr_completion_t * cp, int piece ) { - tr_torrent_t * tor = cp->tor; - uint64_t left; - left = (uint64_t) ( cp->tor->blockCount - cp->blockCount ) * - (uint64_t) tor->blockSize; - if( !tr_bitfieldHas( cp->blockBitfield, cp->tor->blockCount - 1 ) && - tor->info.totalSize % tor->blockSize ) - { - left += tor->info.totalSize % tor->blockSize; - left -= tor->blockSize; - } - return left; + const int total = tr_cpCountBlocks( cp, piece ); + const int have = cp->completeBlocks[piece]; + assert( have <= total ); + return have == total; } -/* Pieces */ -int tr_cpPieceHasAllBlocks( tr_completion_t * cp, int piece ) -{ - tr_torrent_t * tor = cp->tor; - int startBlock = tr_pieceStartBlock( piece ); - int endBlock = startBlock + tr_pieceCountBlocks( piece ); - int i; - - for( i = startBlock; i < endBlock; i++ ) - { - if( !tr_bitfieldHas( cp->blockBitfield, i ) ) - { - return 0; - } - } - return 1; -} -int tr_cpPieceIsComplete( tr_completion_t * cp, int piece ) -{ - return tr_bitfieldHas( cp->pieceBitfield, piece ); -} - -tr_bitfield_t * tr_cpPieceBitfield( tr_completion_t * cp ) +const tr_bitfield_t * tr_cpPieceBitfield( const tr_completion_t * cp ) { return cp->pieceBitfield; } void tr_cpPieceAdd( tr_completion_t * cp, int piece ) { - tr_torrent_t * tor = cp->tor; - int startBlock, endBlock, i; + int i; + const tr_torrent_t * tor = cp->tor; + const int n_blocks = tr_cpCountBlocks( cp, piece ); + const int startBlock = tr_pieceStartBlock( piece ); + const int endBlock = startBlock + n_blocks; - startBlock = tr_pieceStartBlock( piece ); - endBlock = startBlock + tr_pieceCountBlocks( piece ); - for( i = startBlock; i < endBlock; i++ ) - { - tr_cpBlockAdd( cp, i ); - } + cp->completeBlocks[piece] = n_blocks; + + for( i=startBlock; iblockDownloaders[i] ) + --cp->missingBlocks[piece]; + + tr_bitfieldAddRange( cp->blockBitfield, startBlock, endBlock-1 ); tr_bitfieldAdd( cp->pieceBitfield, piece ); } void tr_cpPieceRem( tr_completion_t * cp, int piece ) { - tr_torrent_t * tor = cp->tor; - int startBlock, endBlock, i; + int i; + const tr_torrent_t * tor = cp->tor; + const int n_blocks = tr_cpCountBlocks( cp, piece ); + const int startBlock = tr_pieceStartBlock( piece ); + const int endBlock = startBlock + n_blocks; - startBlock = tr_pieceStartBlock( piece ); - endBlock = startBlock + tr_pieceCountBlocks( piece ); - for( i = startBlock; i < endBlock; i++ ) - { - tr_cpBlockRem( cp, i ); - } + cp->completeBlocks[piece] = 0; + + for( i=startBlock; iblockDownloaders[i] ) + ++cp->missingBlocks[piece]; + + tr_bitfieldRemRange ( cp->blockBitfield, startBlock, endBlock-1 ); tr_bitfieldRem( cp->pieceBitfield, piece ); } @@ -154,7 +161,13 @@ void tr_cpDownloaderAdd( tr_completion_t * cp, int block ) void tr_cpDownloaderRem( tr_completion_t * cp, int block ) { - tr_torrent_t * tor = cp->tor; + tr_torrent_t * tor; + + assert( cp != NULL ); + assert( cp->tor != NULL ); + assert( 0 <= block ); + + tor = cp->tor; (cp->blockDownloaders[block])--; if( !cp->blockDownloaders[block] && !tr_cpBlockIsComplete( cp, block ) ) { @@ -164,129 +177,90 @@ void tr_cpDownloaderRem( tr_completion_t * cp, int block ) int tr_cpBlockIsComplete( const tr_completion_t * cp, int block ) { + assert( cp != NULL ); + assert( 0 <= block ); + return tr_bitfieldHas( cp->blockBitfield, block ); } void tr_cpBlockAdd( tr_completion_t * cp, int block ) { - tr_torrent_t * tor = cp->tor; + const tr_torrent_t * tor; + + assert( cp != NULL ); + assert( cp->tor != NULL ); + assert( 0 <= block ); + + tor = cp->tor; + if( !tr_cpBlockIsComplete( cp, block ) ) { - (cp->blockCount)++; + const int piece = tr_blockPiece( block ); + ++cp->completeBlocks[piece]; + + if( cp->completeBlocks[piece] == tr_cpCountBlocks( cp, piece ) ) + tr_bitfieldAdd( cp->pieceBitfield, piece ); + if( !cp->blockDownloaders[block] ) - { - (cp->missingBlocks[tr_blockPiece(block)])--; - } + cp->missingBlocks[piece]--; + + tr_bitfieldAdd( cp->blockBitfield, block ); } - tr_bitfieldAdd( cp->blockBitfield, block ); } -void tr_cpBlockRem( tr_completion_t * cp, int block ) +const tr_bitfield_t * tr_cpBlockBitfield( const tr_completion_t * cp ) { - tr_torrent_t * tor = cp->tor; - if( tr_cpBlockIsComplete( cp, block ) ) - { - (cp->blockCount)--; - if( !cp->blockDownloaders[block] ) - { - (cp->missingBlocks[tr_blockPiece(block)])++; - } - } - tr_bitfieldRem( cp->blockBitfield, block ); -} + assert( cp != NULL ); -tr_bitfield_t * tr_cpBlockBitfield( tr_completion_t * cp ) -{ return cp->blockBitfield; } -void tr_cpBlockBitfieldSet( tr_completion_t * cp, tr_bitfield_t * bitfield ) +void +tr_cpBlockBitfieldSet( tr_completion_t * cp, tr_bitfield_t * bitfield ) { - tr_torrent_t * tor = cp->tor; - int i, j; - int startBlock, endBlock; - int pieceComplete; + int i; - for( i = 0; i < cp->tor->info.pieceCount; i++ ) - { - startBlock = tr_pieceStartBlock( i ); - endBlock = startBlock + tr_pieceCountBlocks( i ); - pieceComplete = 1; + assert( cp != NULL ); + assert( bitfield != NULL ); - for( j = startBlock; j < endBlock; j++ ) - { - if( tr_bitfieldHas( bitfield, j ) ) - { - tr_cpBlockAdd( cp, j ); - } - else - { - pieceComplete = 0; - } - } - if( pieceComplete ) - { - tr_cpPieceAdd( cp, i ); - } - } + tr_cpReset( cp ); + + for( i=0; i < cp->tor->blockCount; ++i ) + if( tr_bitfieldHas( bitfield, i ) ) + tr_cpBlockAdd( cp, i ); } -float tr_cpPercentBlocksInPiece( tr_completion_t * cp, int piece ) +float tr_cpPercentBlocksInPiece( const tr_completion_t * cp, int piece ) { - tr_torrent_t * tor = cp->tor; - int i; - int blockCount, startBlock, endBlock; - int complete; - tr_bitfield_t * bitfield; - - blockCount = tr_pieceCountBlocks( piece ); - startBlock = tr_pieceStartBlock( piece ); - endBlock = startBlock + blockCount; - complete = 0; - - bitfield = cp->blockBitfield; + assert( cp != NULL ); - for( i = startBlock; i < endBlock; i++ ) - { - if( tr_bitfieldHas( bitfield, i ) ) - { - complete++; - } - } - - return (float)complete / (float)blockCount; + return cp->completeBlocks[piece] / (double)tr_cpCountBlocks( cp, piece ); } int tr_cpMissingBlockInPiece( const tr_completion_t * cp, int piece ) { + int i; const tr_torrent_t * tor = cp->tor; - int start, count, end, i; + const int start = tr_pieceStartBlock( piece ); + const int end = start + tr_cpCountBlocks( cp, piece ); - start = tr_pieceStartBlock( piece ); - count = tr_pieceCountBlocks( piece ); - end = start + count; - - for( i = start; i < end; i++ ) - { - if( tr_cpBlockIsComplete( cp, i ) || cp->blockDownloaders[i] ) - { - continue; - } - return i; - } + for( i = start; i < end; ++i ) + if( !tr_cpBlockIsComplete( cp, i ) && !cp->blockDownloaders[i] ) + return i; return -1; } -int tr_cpMostMissingBlockInPiece( tr_completion_t * cp, int piece, - int * downloaders ) +int tr_cpMostMissingBlockInPiece( const tr_completion_t * cp, + int piece, + int * downloaders ) { tr_torrent_t * tor = cp->tor; int start, count, end, i; int * pool, poolSize, min, ret; start = tr_pieceStartBlock( piece ); - count = tr_pieceCountBlocks( piece ); + count = tr_cpCountBlocks( cp, piece ); end = start + count; pool = malloc( count * sizeof( int ) ); @@ -324,3 +298,129 @@ int tr_cpMostMissingBlockInPiece( tr_completion_t * cp, int piece, return ret; } + +int +tr_cpMissingBlocksForPiece( const tr_completion_t * cp, int piece ) +{ + assert( cp != NULL ); + + return cp->missingBlocks[piece]; +} + +/*** +**** +***/ + +cp_status_t +tr_cpGetStatus ( const tr_completion_t * cp ) +{ + int i; + int ret = TR_CP_COMPLETE; + const tr_info_t * info; + + assert( cp != NULL ); + assert( cp->tor != NULL ); + + info = &cp->tor->info; + for( i=0; ipieceCount; ++i ) { + if( tr_cpPieceIsComplete( cp, i ) ) + continue; + if( info->pieces[i].priority != TR_PRI_DND) + return TR_CP_INCOMPLETE; + ret = TR_CP_DONE; + } + + return ret; +} + +uint64_t +tr_cpLeftUntilComplete ( const tr_completion_t * cp ) +{ + int i; + uint64_t b=0; + const tr_torrent_t * tor; + const tr_info_t * info; + + assert( cp != NULL ); + assert( cp->tor != NULL ); + + tor = cp->tor; + info = &tor->info; + for( i=0; ipieceCount; ++i ) + if( !tr_cpPieceIsComplete( cp, i ) ) + b += ( tr_cpCountBlocks( cp, i ) - cp->completeBlocks[ i ] ); + + return b * tor->blockSize;; +} + +uint64_t +tr_cpLeftUntilDone ( const tr_completion_t * cp ) +{ + int i; + uint64_t b=0; + const tr_torrent_t * tor; + const tr_info_t * info; + + assert( cp != NULL ); + assert( cp->tor != NULL ); + + tor = cp->tor; + info = &tor->info; + + for( i=0; ipieceCount; ++i ) + if( !tr_cpPieceIsComplete( cp, i ) && info->pieces[i].priority != TR_PRI_DND ) + b += tor->blockSize * (tr_cpCountBlocks( cp, i ) - cp->completeBlocks[ i ] ); + + return b; +} + +float +tr_cpPercentComplete ( const tr_completion_t * cp ) +{ + int i; + uint64_t have=0; + const tr_torrent_t * tor; + + assert( cp != NULL ); + assert( cp->tor != NULL ); + + tor = cp->tor; + for( i=0; iinfo.pieceCount; ++i ) + have += cp->completeBlocks[ i ]; + + return tor->blockCount ? (float)have / (float)tor->blockCount : 0.0f; +} + +float +tr_cpPercentDone( const tr_completion_t * cp ) +{ + int i; + uint64_t have=0, total=0; + const tr_torrent_t * tor; + + assert( cp != NULL ); + assert( cp->tor != NULL ); + + tor = cp->tor; + + for( i=0; iinfo.pieceCount; ++i ) { + if( tor->info.pieces[i].priority != TR_PRI_DND) { + total += tr_cpCountBlocks( cp, i ); + have += cp->completeBlocks[ i ]; + } + } + + return !total ? 0.0f : (float)have / (float)total; +} + +uint64_t +tr_cpDownloadedValid( const tr_completion_t * cp ) +{ + int i, n; + uint64_t b=0; + + for( i=0, n=cp->tor->info.pieceCount; icompleteBlocks[ i ]; + + return b * cp->tor->blockSize; +} diff --git a/libtransmission/completion.h b/libtransmission/completion.h index 68846e54d..79acbcb60 100644 --- a/libtransmission/completion.h +++ b/libtransmission/completion.h @@ -22,49 +22,44 @@ * DEALINGS IN THE SOFTWARE. *****************************************************************************/ -struct tr_completion_s -{ - tr_torrent_t * tor; - tr_bitfield_t * blockBitfield; - uint8_t * blockDownloaders; - int blockCount; - tr_bitfield_t * pieceBitfield; - int * missingBlocks; -}; +#ifndef TR_COMPLETION_H +#define TR_COMPLETION_H -tr_completion_t * tr_cpInit( tr_torrent_t * ); -void tr_cpClose( tr_completion_t * ); -void tr_cpReset( tr_completion_t * ); +#include "transmission.h" + +tr_completion_t * tr_cpInit( tr_torrent_t * ); +void tr_cpClose( tr_completion_t * ); +void tr_cpReset( tr_completion_t * ); /* General */ -float tr_cpCompletionAsFloat( tr_completion_t * ); -static inline int tr_cpIsSeeding( tr_completion_t * cp ) -{ - return ( cp->blockCount == cp->tor->blockCount ); -} -uint64_t tr_cpLeftBytes( tr_completion_t * ); + +cp_status_t tr_cpGetStatus ( const tr_completion_t * ); + +uint64_t tr_cpDownloadedValid( const tr_completion_t * ); +uint64_t tr_cpLeftUntilComplete( const tr_completion_t * ); +uint64_t tr_cpLeftUntilDone( const tr_completion_t * ); +float tr_cpPercentComplete( const tr_completion_t * ); +float tr_cpPercentDone( const tr_completion_t * ); /* Pieces */ -int tr_cpPieceHasAllBlocks( tr_completion_t *, int piece ); -int tr_cpPieceIsComplete( tr_completion_t *, int piece ); -tr_bitfield_t * tr_cpPieceBitfield( tr_completion_t * ); -void tr_cpPieceAdd( tr_completion_t *, int piece ); -void tr_cpPieceRem( tr_completion_t *, int piece ); +int tr_cpPieceHasAllBlocks( const tr_completion_t *, int piece ); +int tr_cpPieceIsComplete( const tr_completion_t *, int piece ); +const tr_bitfield_t * tr_cpPieceBitfield( const tr_completion_t* ); +void tr_cpPieceAdd( tr_completion_t *, int piece ); +void tr_cpPieceRem( tr_completion_t *, int piece ); /* Blocks */ -void tr_cpDownloaderAdd( tr_completion_t *, int block ); -void tr_cpDownloaderRem( tr_completion_t *, int block ); -int tr_cpBlockIsComplete( const tr_completion_t *, int block ); -void tr_cpBlockAdd( tr_completion_t *, int block ); -void tr_cpBlockRem( tr_completion_t *, int block ); -tr_bitfield_t * tr_cpBlockBitfield( tr_completion_t * ); -void tr_cpBlockBitfieldSet( tr_completion_t *, tr_bitfield_t * ); -float tr_cpPercentBlocksInPiece( tr_completion_t * cp, int piece ); +void tr_cpDownloaderAdd( tr_completion_t *, int block ); +void tr_cpDownloaderRem( tr_completion_t *, int block ); +int tr_cpBlockIsComplete( const tr_completion_t *, int block ); +void tr_cpBlockAdd( tr_completion_t *, int block ); +const tr_bitfield_t * tr_cpBlockBitfield( const tr_completion_t * ); +void tr_cpBlockBitfieldSet( tr_completion_t *, tr_bitfield_t * ); +float tr_cpPercentBlocksInPiece( const tr_completion_t * cp, int piece ); /* Missing = we don't have it and we are not getting it from any peer yet */ -static inline int tr_cpMissingBlocksForPiece( const tr_completion_t * cp, int piece ) -{ - return cp->missingBlocks[piece]; -} -int tr_cpMissingBlockInPiece( const tr_completion_t *, int piece ); -int tr_cpMostMissingBlockInPiece( tr_completion_t *, int piece, - int * downloaders ); +int tr_cpMissingBlocksForPiece( const tr_completion_t * cp, int piece ); +int tr_cpMissingBlockInPiece( const tr_completion_t *, int piece ); +int tr_cpMostMissingBlockInPiece( const tr_completion_t *, int piece, + int * downloaders ); + +#endif diff --git a/libtransmission/fastresume.c b/libtransmission/fastresume.c new file mode 100644 index 000000000..e5f449ee1 --- /dev/null +++ b/libtransmission/fastresume.c @@ -0,0 +1,415 @@ +/****************************************************************************** + * $Id:$ + * + * Copyright (c) 2005-2007 Transmission authors and contributors + * + * 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, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * 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. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + *****************************************************************************/ + +/*********************************************************************** + * Fast resume + *********************************************************************** + * The format of the resume file is a 4 byte format version (currently 1), + * followed by several variable-sized blocks of data. Each block is + * preceded by a 1 byte ID and a 4 byte length. The currently recognized + * IDs are defined below by the FR_ID_* macros. The length does not include + * the 5 bytes for the ID and length. + * + * The name of the resume file is "resume.-", although + * older files with a name of "resume." will be recognized if + * the former doesn't exist. + * + * All values are stored in the native endianness. Moving a + * libtransmission resume file from an architecture to another will not + * work, although it will not hurt either (the version will be wrong, + * so the resume file will not be read). + **********************************************************************/ + +#include "transmission.h" +#include "fastresume.h" + +/* time_t can be 32 or 64 bits... for consistency we'll hardwire 64 */ +typedef uint64_t tr_time_t; + +/* deprecated */ +#define FR_ID_PROGRESS_SLOTS 0x01 +/* number of bytes downloaded */ +#define FR_ID_DOWNLOADED 0x02 +/* number of bytes uploaded */ +#define FR_ID_UPLOADED 0x03 +/* IPs and ports of connectable peers */ +#define FR_ID_PEERS 0x04 +/* progress data: + * - 4 bytes * number of files: mtimes of files + * - 1 bit * number of blocks: whether we have the block or not + */ +#define FR_ID_PROGRESS 0x05 + +/* macros for the length of various pieces of the progress data */ +#define FR_MTIME_LEN( t ) \ + ( sizeof(tr_time_t) * (t)->info.fileCount ) +#define FR_BLOCK_BITFIELD_LEN( t ) \ + ( ( (t)->blockCount + 7 ) / 8 ) +#define FR_PROGRESS_LEN( t ) \ + ( FR_MTIME_LEN( t ) + FR_BLOCK_BITFIELD_LEN( t ) ) + +static void +fastResumeFileName( char * path, size_t size, const tr_torrent_t * tor, int tag ) +{ + if( tag ) + { + snprintf( path, size, "%s/resume.%s-%s", tr_getCacheDirectory(), + tor->info.hashString, tor->handle->tag ); + } + else + { + snprintf( path, size, "%s/resume.%s", tr_getCacheDirectory(), + tor->info.hashString ); + } +} + +static tr_time_t* +getMTimes( const tr_torrent_t * tor, int * setme_n ) +{ + int i; + const int n = tor->info.fileCount; + tr_time_t * m = calloc( n, sizeof(tr_time_t) ); + + for( i=0; idestination, tor->info.files[i].name ); + if ( !stat( fname, &sb ) && S_ISREG( sb.st_mode ) ) { +#ifdef SYS_DARWIN + m[i] = sb.st_mtimespec.tv_sec; +#else + m[i] = sb.st_mtime; +#endif + } + } + + *setme_n = n; + return m; +} + +static inline void fastResumeWriteData( uint8_t id, void * data, uint32_t size, + uint32_t count, FILE * file ) +{ + uint32_t datalen = size * count; + + fwrite( &id, 1, 1, file ); + fwrite( &datalen, 4, 1, file ); + fwrite( data, size, count, file ); +} + +void fastResumeSave( const tr_torrent_t * tor ) +{ + char path[MAX_PATH_LENGTH]; + FILE * file; + const int version = 1; + uint64_t total; + + fastResumeFileName( path, sizeof path, tor, 1 ); + file = fopen( path, "w" ); + if( NULL == file ) { + tr_err( "Couldn't open '%s' for writing", path ); + return; + } + + /* Write format version */ + fwrite( &version, 4, 1, file ); + + /* Write progress data */ + if (1) { + int n; + tr_time_t * mtimes; + uint8_t * buf = malloc( FR_PROGRESS_LEN( tor ) ); + uint8_t * walk = buf; + const tr_bitfield_t * bitfield; + + /* mtimes */ + mtimes = getMTimes( tor, &n ); + memcpy( walk, mtimes, n*sizeof(tr_time_t) ); + walk += n * sizeof(tr_time_t); + + /* completion bitfield */ + bitfield = tr_cpBlockBitfield( tor->completion ); + assert( (unsigned)FR_BLOCK_BITFIELD_LEN( tor ) == bitfield->len ); + memcpy( walk, bitfield->bits, bitfield->len ); + walk += bitfield->len; + + /* write it */ + assert( walk-buf == (int)FR_PROGRESS_LEN( tor ) ); + fastResumeWriteData( FR_ID_PROGRESS, buf, 1, walk-buf, file ); + + /* cleanup */ + free( mtimes ); + free( buf ); + } + + /* Write download and upload totals */ + total = tor->downloadedCur + tor->downloadedPrev; + fastResumeWriteData( FR_ID_DOWNLOADED, &total, 8, 1, file ); + total = tor->uploadedCur + tor->uploadedPrev; + fastResumeWriteData( FR_ID_UPLOADED, &total, 8, 1, file ); + + if( !( TR_FLAG_PRIVATE & tor->info.flags ) ) + { + /* Write IPs and ports of connectable peers, if any */ + int size; + uint8_t * buf = NULL; + if( ( size = tr_peerGetConnectable( tor, &buf ) ) > 0 ) + { + fastResumeWriteData( FR_ID_PEERS, buf, size, 1, file ); + free( buf ); + } + } + + fclose( file ); + + tr_dbg( "Resume file '%s' written", path ); +} + +static int +fastResumeLoadProgress( const tr_torrent_t * tor, + tr_bitfield_t * uncheckedPieces, + FILE * file ) +{ + const size_t len = FR_PROGRESS_LEN( tor ); + uint8_t * buf = calloc( len, 1 ); + uint8_t * walk = buf; + + if( len != fread( buf, 1, len, file ) ) { + tr_inf( "Couldn't read from resume file" ); + free( buf ); + return TR_ERROR_IO_OTHER; + } + + /* compare file mtimes */ + if (1) { + int i, n; + tr_time_t * curMTimes = getMTimes( tor, &n ); + const tr_time_t * oldMTimes = (const tr_time_t *) walk; + for( i=0; iinfo.files[i]; + tr_inf( "File '%s' mtimes differ-- flaggin pieces [%d..%d]", + file->name, file->firstPiece, file->lastPiece); + tr_bitfieldAddRange( uncheckedPieces, + file->firstPiece, file->lastPiece ); + } + } + free( curMTimes ); + walk += n * sizeof(tr_time_t); + } + + /* get the completion bitfield */ + if (1) { + tr_bitfield_t bitfield; + memset( &bitfield, 0, sizeof bitfield ); + bitfield.len = FR_BLOCK_BITFIELD_LEN( tor ); + bitfield.bits = walk; + tr_cpBlockBitfieldSet( tor->completion, &bitfield ); + } + + free( buf ); + return TR_OK; +} + +static int +fastResumeLoadOld( tr_torrent_t * tor, + tr_bitfield_t * uncheckedPieces, + FILE * file ) +{ + /* Check the size */ + const int size = 4 + FR_PROGRESS_LEN( tor ); + fseek( file, 0, SEEK_END ); + if( ftell( file ) != size ) + { + tr_inf( "Wrong size for resume file (%d bytes, %d expected)", + (int)ftell( file ), size ); + fclose( file ); + return 1; + } + + /* load progress information */ + fseek( file, 4, SEEK_SET ); + if( fastResumeLoadProgress( tor, uncheckedPieces, file ) ) + { + fclose( file ); + return 1; + } + + fclose( file ); + + tr_inf( "Fast resuming successful (version 0)" ); + + return 0; +} + +int +fastResumeLoad( tr_torrent_t * tor, + tr_bitfield_t * uncheckedPieces ) +{ + char path[MAX_PATH_LENGTH]; + FILE * file; + int version = 0; + uint8_t id; + uint32_t len; + int ret; + + assert( tor != NULL ); + assert( uncheckedPieces != NULL ); + + /* Open resume file */ + fastResumeFileName( path, sizeof path, tor, 1 ); + file = fopen( path, "r" ); + if( NULL == file ) + { + if( ENOENT == errno ) + { + fastResumeFileName( path, sizeof path, tor, 0 ); + file = fopen( path, "r" ); + if( NULL != file ) + { + goto good; + } + fastResumeFileName( path, sizeof path, tor, 1 ); + } + tr_inf( "Could not open '%s' for reading", path ); + return 1; + } + good: + tr_dbg( "Resume file '%s' loaded", path ); + + /* Check format version */ + fread( &version, 4, 1, file ); + if( 0 == version ) + { + return fastResumeLoadOld( tor, uncheckedPieces, file ); + } + if( 1 != version ) + { + tr_inf( "Resume file has version %d, not supported", version ); + fclose( file ); + return 1; + } + + ret = 1; + /* read each block of data */ + while( 1 == fread( &id, 1, 1, file ) && 1 == fread( &len, 4, 1, file ) ) + { + switch( id ) + { + case FR_ID_PROGRESS: + /* read progress data */ + if( (uint32_t)FR_PROGRESS_LEN( tor ) == len ) + { + if( fastResumeLoadProgress( tor, uncheckedPieces, file ) ) + { + if( feof( file ) || ferror( file ) ) + { + fclose( file ); + return 1; + } + } + else + { + ret = 0; + } + continue; + } + break; + + case FR_ID_DOWNLOADED: + /* read download total */ + if( 8 == len) + { + if( 1 != fread( &tor->downloadedPrev, 8, 1, file ) ) + { + fclose( file ); + return 1; + } + continue; + } + break; + + case FR_ID_UPLOADED: + /* read upload total */ + if( 8 == len) + { + if( 1 != fread( &tor->uploadedPrev, 8, 1, file ) ) + { + fclose( file ); + return 1; + } + continue; + } + break; + + case FR_ID_PEERS: + if( !( TR_FLAG_PRIVATE & tor->info.flags ) ) + { + int used; + uint8_t * buf = malloc( len ); + if( 1 != fread( buf, len, 1, file ) ) + { + free( buf ); + fclose( file ); + return 1; + } + used = tr_torrentAddCompact( tor, TR_PEER_FROM_CACHE, + buf, len / 6 ); + tr_dbg( "found %i peers in resume file, used %i", + len / 6, used ); + free( buf ); + } + continue; + + default: + break; + } + + /* if we didn't read the data, seek past it */ + tr_inf( "Skipping resume data type %02x, %u bytes", id, len ); + fseek( file, len, SEEK_CUR ); + } + + fclose( file ); + + if( !ret ) + { + tr_inf( "Fast resuming successful" ); + } + + return ret; +} + +void +fastResumeRemove( tr_torrent_t * tor ) +{ + char file[MAX_PATH_LENGTH]; + fastResumeFileName( file, sizeof file, tor, NULL != tor->handle->tag ); + + if ( unlink( file ) ) + { + tr_inf( "Removing fast resume file failed" ); + } +} diff --git a/libtransmission/fastresume.h b/libtransmission/fastresume.h index 3289e9d49..8cb915255 100644 --- a/libtransmission/fastresume.h +++ b/libtransmission/fastresume.h @@ -22,421 +22,14 @@ * DEALINGS IN THE SOFTWARE. *****************************************************************************/ -/*********************************************************************** - * Fast resume - *********************************************************************** - * The format of the resume file is a 4 byte format version (currently 1), - * followed by several variable-sized blocks of data. Each block is - * preceded by a 1 byte ID and a 4 byte length. The currently recognized - * IDs are defined below by the FR_ID_* macros. The length does not include - * the 5 bytes for the ID and length. - * - * The name of the resume file is "resume.-", although - * older files with a name of "resume." will be recognized if - * the former doesn't exist. - * - * All values are stored in the native endianness. Moving a - * libtransmission resume file from an architecture to another will not - * work, although it will not hurt either (the version will be wrong, - * so the resume file will not be read). - **********************************************************************/ +#ifndef TR_FAST_RESUME_H +#define TR_FAST_RESUME_H -/* progress data: - * - 4 bytes * number of files: mtimes of files - * - 1 bit * number of blocks: whether we have the block or not - * - 4 bytes * number of pieces (byte aligned): the pieces that have - * been completed or started in each slot - */ -#define FR_ID_PROGRESS 0x01 -/* number of bytes downloaded */ -#define FR_ID_DOWNLOADED 0x02 -/* number of bytes uploaded */ -#define FR_ID_UPLOADED 0x03 -/* IPs and ports of connectable peers */ -#define FR_ID_PEERS 0x04 +void fastResumeSave( const tr_torrent_t * tor ); -/* macros for the length of various pieces of the progress data */ -#define FR_MTIME_LEN( t ) \ - ( 4 * (t)->info.fileCount ) -#define FR_BLOCK_BITFIELD_LEN( t ) \ - ( ( (t)->blockCount + 7 ) / 8 ) -#define FR_SLOTPIECE_LEN( t ) \ - ( 4 * (t)->info.pieceCount ) -#define FR_PROGRESS_LEN( t ) \ - ( FR_MTIME_LEN( t ) + FR_BLOCK_BITFIELD_LEN( t ) + FR_SLOTPIECE_LEN( t ) ) +int fastResumeLoad( tr_torrent_t * tor, + tr_bitfield_t * uncheckedPieces ); -static void -fastResumeFileName( char * path, size_t size, tr_torrent_t * tor, int tag ) -{ - if( tag ) - { - snprintf( path, size, "%s/resume.%s-%s", tr_getCacheDirectory(), - tor->info.hashString, tor->handle->tag ); - } - else - { - snprintf( path, size, "%s/resume.%s", tr_getCacheDirectory(), - tor->info.hashString ); - } -} +void fastResumeRemove( tr_torrent_t * tor ); -static int fastResumeMTimes( tr_io_t * io, int * tab ) -{ - tr_torrent_t * tor = io->tor; - tr_info_t * inf = &tor->info; - - int i; - char * path; - struct stat sb; - - for( i = 0; i < inf->fileCount; i++ ) - { - asprintf( &path, "%s/%s", tor->destination, inf->files[i].name ); - if( stat( path, &sb ) ) - { - tab[i] = 0xFFFFFFFF; - } - else if( S_ISREG( sb.st_mode ) ) - { -#ifdef SYS_DARWIN - tab[i] = ( sb.st_mtimespec.tv_sec & 0x7FFFFFFF ); -#else - tab[i] = ( sb.st_mtime & 0x7FFFFFFF ); #endif - } - else - { - /* Empty folder */ - tab[i] = 0; - } - free( path ); - } - - return 0; -} - -static inline void fastResumeWriteData( uint8_t id, void * data, uint32_t size, - uint32_t count, FILE * file ) -{ - uint32_t datalen = size * count; - - fwrite( &id, 1, 1, file ); - fwrite( &datalen, 4, 1, file ); - fwrite( data, size, count, file ); -} - -static void fastResumeSave( tr_io_t * io ) -{ - tr_torrent_t * tor = io->tor; - - char path[MAX_PATH_LENGTH]; - FILE * file; - int version = 1; - uint8_t * buf; - uint64_t total; - int size; - tr_bitfield_t * bitfield; - - buf = malloc( FR_PROGRESS_LEN( tor ) ); - - /* Get file sizes */ - if( fastResumeMTimes( io, (int*)buf ) ) - { - free( buf ); - return; - } - - /* Create/overwrite the resume file */ - fastResumeFileName( path, sizeof path, tor, 1 ); - file = fopen( path, "w" ); - if( NULL == file ) - { - tr_err( "Could not open '%s' for writing", path ); - free( buf ); - return; - } - - /* Write format version */ - fwrite( &version, 4, 1, file ); - - /* Build and copy the bitfield for blocks */ - bitfield = tr_cpBlockBitfield( tor->completion ); - assert( FR_BLOCK_BITFIELD_LEN( tor ) == bitfield->len ); - memcpy(buf + FR_MTIME_LEN( tor ), bitfield->bits, bitfield->len ); - - /* Copy the 'slotPiece' table */ - memcpy(buf + FR_MTIME_LEN( tor ) + FR_BLOCK_BITFIELD_LEN( tor ), - io->slotPiece, FR_SLOTPIECE_LEN( tor ) ); - - /* Write progress data */ - fastResumeWriteData( FR_ID_PROGRESS, buf, 1, FR_PROGRESS_LEN( tor ), file ); - free( buf ); - - /* Write download and upload totals */ - total = tor->downloadedCur + tor->downloadedPrev; - fastResumeWriteData( FR_ID_DOWNLOADED, &total, 8, 1, file ); - total = tor->uploadedCur + tor->uploadedPrev; - fastResumeWriteData( FR_ID_UPLOADED, &total, 8, 1, file ); - - if( !( TR_FLAG_PRIVATE & tor->info.flags ) ) - { - /* Write IPs and ports of connectable peers, if any */ - if( ( size = tr_peerGetConnectable( tor, &buf ) ) > 0 ) - { - fastResumeWriteData( FR_ID_PEERS, buf, size, 1, file ); - free( buf ); - } - } - - fclose( file ); - - tr_dbg( "Resume file '%s' written", path ); -} - -static int fastResumeLoadProgress( tr_io_t * io, FILE * file ) -{ - tr_torrent_t * tor = io->tor; - tr_info_t * inf = &tor->info; - - int * fileMTimes; - int i, j; - uint8_t * buf; - size_t len; - tr_bitfield_t bitfield; - - len = FR_PROGRESS_LEN( tor ); - buf = calloc( len, 1 ); - if( len != fread( buf, 1, len, file ) ) - { - tr_inf( "Could not read from resume file" ); - free( buf ); - return 1; - } - - /* Compare file mtimes */ - fileMTimes = malloc( FR_MTIME_LEN( tor ) ); - if( fastResumeMTimes( io, fileMTimes ) ) - { - free( buf ); - free( fileMTimes ); - return 1; - } - if( memcmp( fileMTimes, buf, FR_MTIME_LEN( tor ) ) ) - { - tr_inf( "File mtimes don't match" ); - free( buf ); - free( fileMTimes ); - return 1; - } - free( fileMTimes ); - - /* Copy the bitfield for blocks and fill blockHave */ - memset( &bitfield, 0, sizeof bitfield ); - bitfield.len = FR_BLOCK_BITFIELD_LEN( tor ); - bitfield.bits = buf + FR_MTIME_LEN( tor ); - tr_cpBlockBitfieldSet( tor->completion, &bitfield ); - - /* Copy the 'slotPiece' table */ - memcpy( io->slotPiece, buf + FR_MTIME_LEN( tor ) + - FR_BLOCK_BITFIELD_LEN( tor ), FR_SLOTPIECE_LEN( tor ) ); - - free( buf ); - - /* Update io->pieceSlot, io->slotsUsed, and tor->bitfield */ - io->slotsUsed = 0; - for( i = 0; i < inf->pieceCount; i++ ) - { - io->pieceSlot[i] = -1; - for( j = 0; j < inf->pieceCount; j++ ) - { - if( io->slotPiece[j] == i ) - { - // tr_dbg( "Has piece %d in slot %d", i, j ); - io->pieceSlot[i] = j; - io->slotsUsed = MAX( io->slotsUsed, j + 1 ); - break; - } - } - } - // tr_dbg( "Slot used: %d", io->slotsUsed ); - - return 0; -} - -static int fastResumeLoadOld( tr_io_t * io, FILE * file ) -{ - tr_torrent_t * tor = io->tor; - - int size; - - /* Check the size */ - size = 4 + FR_PROGRESS_LEN( tor ); - fseek( file, 0, SEEK_END ); - if( ftell( file ) != size ) - { - tr_inf( "Wrong size for resume file (%d bytes, %d expected)", - (int)ftell( file ), size ); - fclose( file ); - return 1; - } - - /* load progress information */ - fseek( file, 4, SEEK_SET ); - if( fastResumeLoadProgress( io, file ) ) - { - fclose( file ); - return 1; - } - - fclose( file ); - - tr_inf( "Fast resuming successful (version 0)" ); - - return 0; -} - -static int fastResumeLoad( tr_io_t * io ) -{ - tr_torrent_t * tor = io->tor; - - char path[MAX_PATH_LENGTH]; - FILE * file; - int version = 0; - uint8_t id; - uint32_t len; - int ret; - - /* Open resume file */ - fastResumeFileName( path, sizeof path, tor, 1 ); - file = fopen( path, "r" ); - if( NULL == file ) - { - if( ENOENT == errno ) - { - fastResumeFileName( path, sizeof path, tor, 0 ); - file = fopen( path, "r" ); - if( NULL != file ) - { - goto good; - } - fastResumeFileName( path, sizeof path, tor, 1 ); - } - tr_inf( "Could not open '%s' for reading", path ); - return 1; - } - good: - tr_dbg( "Resume file '%s' loaded", path ); - - /* Check format version */ - fread( &version, 4, 1, file ); - if( 0 == version ) - { - return fastResumeLoadOld( io, file ); - } - if( 1 != version ) - { - tr_inf( "Resume file has version %d, not supported", version ); - fclose( file ); - return 1; - } - - ret = 1; - /* read each block of data */ - while( 1 == fread( &id, 1, 1, file ) && 1 == fread( &len, 4, 1, file ) ) - { - switch( id ) - { - case FR_ID_PROGRESS: - /* read progress data */ - if( (uint32_t)FR_PROGRESS_LEN( tor ) == len ) - { - if( fastResumeLoadProgress( io, file ) ) - { - if( feof( file ) || ferror( file ) ) - { - fclose( file ); - return 1; - } - } - else - { - ret = 0; - } - continue; - } - break; - - case FR_ID_DOWNLOADED: - /* read download total */ - if( 8 == len) - { - if( 1 != fread( &tor->downloadedPrev, 8, 1, file ) ) - { - fclose( file ); - return 1; - } - continue; - } - break; - - case FR_ID_UPLOADED: - /* read upload total */ - if( 8 == len) - { - if( 1 != fread( &tor->uploadedPrev, 8, 1, file ) ) - { - fclose( file ); - return 1; - } - continue; - } - break; - - case FR_ID_PEERS: - if( !( TR_FLAG_PRIVATE & tor->info.flags ) ) - { - int used; - uint8_t * buf = malloc( len ); - if( 1 != fread( buf, len, 1, file ) ) - { - free( buf ); - fclose( file ); - return 1; - } - used = tr_torrentAddCompact( tor, TR_PEER_FROM_CACHE, - buf, len / 6 ); - tr_dbg( "found %i peers in resume file, used %i", - len / 6, used ); - free( buf ); - } - continue; - - default: - break; - } - - /* if we didn't read the data, seek past it */ - tr_inf( "Skipping resume data type %02x, %u bytes", id, len ); - fseek( file, len, SEEK_CUR ); - } - - fclose( file ); - - if( !ret ) - { - tr_inf( "Fast resuming successful" ); - } - - return ret; -} - -static void fastResumeRemove( tr_torrent_t * tor ) -{ - char file[MAX_PATH_LENGTH]; - fastResumeFileName( file, sizeof file, tor, NULL != tor->handle->tag ); - - if ( unlink( file ) ) - { - tr_inf( "Removing fast resume file failed" ); - } -} diff --git a/libtransmission/inout.c b/libtransmission/inout.c index 5ffb110cd..8bafa3663 100644 --- a/libtransmission/inout.c +++ b/libtransmission/inout.c @@ -1,26 +1,12 @@ -/****************************************************************************** - * $Id$ +/* + * This file Copyright (C) 2007 Charles Kerr * - * Copyright (c) 2005-2007 Transmission authors and contributors - * - * 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, sublicense, - * and/or sell copies of the Software, and to permit persons to whom the - * Software is furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * 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. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER - * DEALINGS IN THE SOFTWARE. - *****************************************************************************/ + * This file is licensed by the GPL version 2. Works owned by the + * Transmission project are granted a special exemption to clause 2(b) + * so that the bulk of its code can remain under the MIT license. + * This exemption does not extend to derived works not owned by + * the Transmission project. + */ #include /* for calloc */ #include "transmission.h" @@ -29,737 +15,312 @@ struct tr_io_s { tr_torrent_t * tor; - /* Position of pieces - -1 = we haven't started to download this piece yet - n = we have started or completed the piece in slot n */ - int * pieceSlot; - - /* Pieces in slot - -1 = unused slot - n = piece n */ - int * slotPiece; - - int slotsUsed; - - int writeCount; - int readCount; - int reorderCount; - int hitCount; - - /* private flag to keep from saving aborted fastResume data */ - char checkFilesAborted; + tr_bitfield_t * uncheckedPieces; }; #include "fastresume.h" -/*********************************************************************** - * Local prototypes - **********************************************************************/ -static int checkFiles( tr_io_t * ); -static void closeFiles( tr_io_t * ); -static int readOrWriteBytes( tr_io_t *, uint64_t, int, uint8_t *, int ); -static int readOrWriteSlot( tr_io_t * io, int slot, uint8_t * buf, - int * size, int write ); -static void findSlotForPiece( tr_io_t *, int ); +/**** +***** Low-level IO functions +****/ -#define readBytes(io,o,s,b) readOrWriteBytes(io,o,s,b,0) -#define writeBytes(io,o,s,b) readOrWriteBytes(io,o,s,b,1) +enum { TR_IO_READ, TR_IO_WRITE }; -#define readSlot(io,sl,b,s) readOrWriteSlot(io,sl,b,s,0) -#define writeSlot(io,sl,b,s) readOrWriteSlot(io,sl,b,s,1) - -static int reorderPieces( tr_io_t * io ); - -/*********************************************************************** - * tr_ioLoadResume - *********************************************************************** - * Try to load the fast resume file - **********************************************************************/ -void tr_ioLoadResume( tr_torrent_t * tor ) +static int +readOrWriteBytes ( const tr_torrent_t * tor, + int ioMode, + int fileIndex, + size_t fileOffset, + void * buf, + size_t buflen ) { - tr_io_t * io; - tr_info_t * inf = &tor->info; + const tr_info_t * info = &tor->info; + const tr_file_t * file = &info->files[fileIndex]; + int fd, ret; + typedef size_t (* iofunc) ( int, void *, size_t ); + iofunc func = ioMode == TR_IO_READ ? (iofunc)read : (iofunc)write; - io = calloc( 1, sizeof( tr_io_t ) ); + assert ( 0<=fileIndex && fileIndexfileCount ); + assert ( !file->length || (fileOffset < file->length)); + assert ( fileOffset + buflen <= file->length ); + + if( !file->length ) + return 0; + else if ((fd = tr_fdFileOpen ( tor->destination, file->name, TRUE )) < 0) + ret = fd; + else if( lseek( fd, fileOffset, SEEK_SET ) == ((off_t)-1) ) + ret = TR_ERROR_IO_OTHER; + else if( func( fd, buf, buflen ) != buflen ) + ret = tr_ioErrorFromErrno (); + else + ret = TR_OK; + + if( fd >= 0 ) + tr_fdFileRelease( fd ); + + return ret; +} + +static void +findFileLocation ( const tr_torrent_t * tor, + int pieceIndex, size_t pieceOffset, + int * fileIndex, size_t * fileOffset ) +{ + const tr_info_t * info = &tor->info; + + int i; + uint64_t piecePos = ((uint64_t)pieceIndex * info->pieceSize) + pieceOffset; + + assert ( 0<=pieceIndex && pieceIndex < info->pieceCount ); + assert ( pieceOffset < (size_t)tr_pieceSize(pieceIndex) ); + assert ( piecePos < info->totalSize ); + + for ( i=0; info->files[i].length<=piecePos; ++i ) + piecePos -= info->files[i].length; + + *fileIndex = i; + *fileOffset = piecePos; + + assert ( 0<=*fileIndex && *fileIndexfileCount ); + assert ( *fileOffset < info->files[i].length ); +} + +static int +ensureMinimumFileSize ( const tr_torrent_t * tor, + int fileIndex, + size_t minSize ) /* in bytes */ +{ + int fd; + int ret; + struct stat sb; + const tr_file_t * file = &tor->info.files[fileIndex]; + + assert ( 0<=fileIndex && fileIndexinfo.fileCount ); + assert ( minSize <= file->length ); + + fd = tr_fdFileOpen( tor->destination, file->name, TRUE ); + if( fd < 0 ) /* bad fd */ + ret = fd; + else if (fstat (fd, &sb) ) /* how big is the file? */ + ret = tr_ioErrorFromErrno (); + else if ((size_t)sb.st_size >= minSize) /* already big enough */ + ret = TR_OK; + else if (!ftruncate( fd, minSize )) /* grow it */ + ret = TR_OK; + else /* couldn't grow it */ + ret = tr_ioErrorFromErrno (); + + if( fd >= 0 ) + tr_fdFileRelease( fd ); + + return ret; +} + +static int +readOrWritePiece ( tr_torrent_t * tor, + int ioMode, + int pieceIndex, + size_t pieceOffset, + uint8_t * buf, + size_t buflen ) +{ + int ret = 0; + int fileIndex; + size_t fileOffset; + const tr_info_t * info = &tor->info; + + assert( 0<=pieceIndex && pieceIndexinfo.pieceCount ); + assert( buflen <= (size_t) tr_pieceSize( pieceIndex ) ); + + /* Release the torrent lock so the UI can still update itself if + this blocks for a while */ + tr_lockUnlock( &tor->lock ); + + findFileLocation ( tor, pieceIndex, pieceOffset, &fileIndex, &fileOffset ); + + while( buflen && !ret ) + { + const tr_file_t * file = &info->files[fileIndex]; + const size_t bytesThisPass = MIN( buflen, file->length - fileOffset ); + + if( ioMode == TR_IO_WRITE ) + ret = ensureMinimumFileSize( tor, fileIndex, + fileOffset + bytesThisPass ); + if( !ret ) + ret = readOrWriteBytes( tor, ioMode, + fileIndex, fileOffset, buf, bytesThisPass ); + buf += bytesThisPass; + buflen -= bytesThisPass; + fileIndex++; + fileOffset = 0; + } + + tr_lockLock( &tor->lock ); + + return ret; +} + +int +tr_ioRead( tr_io_t * io, int pieceIndex, int begin, int len, uint8_t * buf ) +{ + return readOrWritePiece ( io->tor, TR_IO_READ, pieceIndex, begin, buf, len ); +} + +int +tr_ioWrite( tr_io_t * io, int pieceIndex, int begin, int len, uint8_t * buf ) +{ + return readOrWritePiece ( io->tor, TR_IO_WRITE, pieceIndex, begin, buf, len ); +} + +/**** +***** +****/ + +static int +tr_ioRecalculateHash ( tr_torrent_t * tor, + int pieceIndex, + uint8_t * setme ) +{ + int n; + int ret; + uint8_t * buf; + const tr_info_t * info; + + assert( tor != NULL ); + assert( setme != NULL ); + assert( 0<=pieceIndex && pieceIndexinfo.pieceCount ); + + info = &tor->info; + n = tr_pieceSize( pieceIndex ); + + buf = malloc( n ); + ret = readOrWritePiece ( tor, TR_IO_READ, pieceIndex, 0, buf, n ); + if( !ret ) { + SHA1( buf, n, setme ); + } + free( buf ); + + return ret; +} + +static int +checkPiece ( tr_torrent_t * tor, int pieceIndex ) +{ + uint8_t hash[SHA_DIGEST_LENGTH]; + int ret = tr_ioRecalculateHash( tor, pieceIndex, hash ) + || memcmp( hash, tor->info.pieces[pieceIndex].hash, SHA_DIGEST_LENGTH ); + tr_dbg ("torrent [%s] piece %d hash check: %s", + tor->info.name, pieceIndex, (ret?"FAILED":"OK")); + return ret; +} + +static void +checkFiles( tr_io_t * io ) +{ + int i; + tr_torrent_t * tor = io->tor; + + tr_bitfieldClear( io->uncheckedPieces ); + + if ( fastResumeLoad( io->tor, io->uncheckedPieces ) ) + tr_bitfieldAddRange( io->uncheckedPieces, 0, tor->info.pieceCount-1 ); + + for( i=0; iinfo.pieceCount; ++i ) + { + if( tor->status & TR_STATUS_STOPPING ) + break; + + if( !tr_bitfieldHas( io->uncheckedPieces, i ) ) + continue; + + tr_inf ( "Checking piece %d because it's not in fast-resume", i ); + + if( !checkPiece( tor, i ) ) + tr_cpPieceAdd( tor->completion, i ); + else + tr_cpPieceRem( tor->completion, i ); + + tr_bitfieldRem( io->uncheckedPieces, i ); + } +} + +/**** +***** Life Cycle +****/ + +tr_io_t* +tr_ioInit( tr_torrent_t * tor ) +{ + tr_io_t * io = calloc( 1, sizeof( tr_io_t ) ); + io->uncheckedPieces = tr_bitfieldNew( tor->info.pieceCount ); io->tor = tor; + checkFiles( io ); + return io; +} - io->pieceSlot = malloc( inf->pieceCount * sizeof( int ) ); - io->slotPiece = malloc( inf->pieceCount * sizeof( int ) ); +void +tr_ioSync( tr_io_t * io ) +{ + int i; + const tr_info_t * info = &io->tor->info; - fastResumeLoad( io ); + for( i=0; ifileCount; ++i ) + tr_fdFileClose( io->tor->destination, info->files[i].name ); + + if( tr_bitfieldIsEmpty( io->uncheckedPieces ) ) + fastResumeSave( io->tor ); +} + +void +tr_ioClose( tr_io_t * io ) +{ + tr_ioSync( io ); + + tr_bitfieldFree( io->uncheckedPieces ); + free( io ); +} + + +/* try to load the fast resume file */ +void +tr_ioLoadResume( tr_torrent_t * tor ) +{ + tr_io_t * io = calloc( 1, sizeof( tr_io_t ) ); + io->uncheckedPieces = tr_bitfieldNew( tor->info.pieceCount ); + io->tor = tor; + fastResumeLoad( tor, io->uncheckedPieces ); tor->ioLoaded = 1; - - free( io->pieceSlot ); - free( io->slotPiece ); + tr_bitfieldFree( io->uncheckedPieces ); free( io ); } void tr_ioRemoveResume( tr_torrent_t * tor ) { if( !tor->io ) - { fastResumeRemove( tor ); - } } -/*********************************************************************** - * tr_ioInit - *********************************************************************** - * Open all files we are going to write to - **********************************************************************/ -tr_io_t * tr_ioInit( tr_torrent_t * tor ) +int +tr_ioHash( tr_io_t * io, int pieceIndex ) { - tr_io_t * io; + int i; - io = calloc( 1, sizeof( tr_io_t ) ); - io->tor = tor; - - if( checkFiles( io ) ) - { - free( io ); - io = NULL; - } - - return io; -} - -/*********************************************************************** - * tr_ioRead - **********************************************************************/ -int tr_ioRead( tr_io_t * io, int index, int begin, int length, - uint8_t * buf ) -{ - tr_info_t * inf = &io->tor->info; - uint64_t offset; - - offset = (uint64_t) io->pieceSlot[index] * - (uint64_t) inf->pieceSize + (uint64_t) begin; - - return readBytes( io, offset, length, buf ); -} - -/*********************************************************************** - * tr_ioWrite - **********************************************************************/ -int tr_ioWrite( tr_io_t * io, int index, int begin, int length, - uint8_t * buf ) -{ - tr_info_t * inf = &io->tor->info; - uint64_t offset; - int rv; - int reorder = 0; - int slotsUsedOld; - - slotsUsedOld = io->slotsUsed; - - if( io->pieceSlot[index] < 0 ) - { - findSlotForPiece( io, index ); - tr_inf( "Piece %d: starting in slot %d", index, - io->pieceSlot[index] ); - reorder = 1; - io->writeCount += 1; - } - - offset = (uint64_t) io->pieceSlot[index] * - (uint64_t) inf->pieceSize + (uint64_t) begin; - - rv = writeBytes( io, offset, length, buf ); - - if (reorder) - { - int reorderRuns; - - tr_dbg( "reorder pieces"); - reorderRuns = reorderPieces( io ); - - if (io->slotsUsed == slotsUsedOld && reorderRuns > 0) - tr_err( "reorder runs should have been 0 but was: %d", reorderRuns ); - -// tr_inf( "STAT: filesize: %03d writeCount: %03d readCount: %03d hitCount: %03d reorderCount: %03d", -// io->slotsUsed,io->writeCount,io->readCount,io->hitCount,io->reorderCount); - } - - return rv; -} - -/*********************************************************************** - * tr_ioHash - **********************************************************************/ -int tr_ioHash( tr_io_t * io, int index ) -{ tr_torrent_t * tor = io->tor; - tr_info_t * inf = &io->tor->info; - int pieceSize; - uint8_t * pieceBuf; - uint8_t hash[SHA_DIGEST_LENGTH]; - int hashFailed; - int ret; - int i; - - pieceSize = tr_pieceSize( index ); - pieceBuf = malloc( pieceSize ); - if( ( ret = readBytes( io, (uint64_t) io->pieceSlot[index] * - (uint64_t) inf->pieceSize, pieceSize, pieceBuf ) ) ) + const int success = !checkPiece( tor, pieceIndex ); + if( success ) { - free( pieceBuf ); - return ret; - } - SHA1( pieceBuf, pieceSize, hash ); - free( pieceBuf ); - - hashFailed = memcmp( hash, &inf->pieces[20*index], SHA_DIGEST_LENGTH ); - if( hashFailed ) - { - tr_err( "Piece %d (slot %d): hash FAILED", index, - io->pieceSlot[index] ); - tr_cpPieceRem( tor->completion, index ); + tr_inf( "Piece %d hash OK", pieceIndex ); + tr_cpPieceAdd( tor->completion, pieceIndex ); } else { - tr_inf( "Piece %d (slot %d): hash OK", index, - io->pieceSlot[index] ); - tr_cpPieceAdd( tor->completion, index ); + tr_err( "Piece %d hash FAILED", pieceIndex ); + tr_cpPieceRem( tor->completion, pieceIndex ); } /* Assign blame or credit to peers */ - for( i = 0; i < tor->peerCount; i++ ) - { - tr_peerBlame( tor->peers[i], index, !hashFailed ); - } + for( i = 0; i < tor->peerCount; ++i ) + tr_peerBlame( tor->peers[i], pieceIndex, success ); return 0; } - -/*********************************************************************** - * tr_ioSync - **********************************************************************/ -void tr_ioSync( tr_io_t * io ) -{ - closeFiles( io ); - - if( !io->checkFilesAborted ) - { - fastResumeSave( io ); - } -} - -/*********************************************************************** - * tr_ioClose - **********************************************************************/ -void tr_ioClose( tr_io_t * io ) -{ - tr_ioSync( io ); - - free( io->pieceSlot ); - free( io->slotPiece ); - free( io ); -} - -/*********************************************************************** - * checkFiles - *********************************************************************** - * Look for pieces - **********************************************************************/ -static int checkFiles( tr_io_t * io ) -{ - tr_torrent_t * tor = io->tor; - tr_info_t * inf = &tor->info; - - int i; - uint8_t * buf; - uint8_t hash[SHA_DIGEST_LENGTH]; - struct stat sb; - - io->pieceSlot = malloc( inf->pieceCount * sizeof( int ) ); - io->slotPiece = malloc( inf->pieceCount * sizeof( int ) ); - io->checkFilesAborted = 0; - - if( !fastResumeLoad( io ) ) - { - return 0; - } - - tr_dbg( "Checking pieces..." ); - - /* Yet we don't have anything */ - memset( io->pieceSlot, 0xFF, inf->pieceCount * sizeof( int ) ); - memset( io->slotPiece, 0xFF, inf->pieceCount * sizeof( int ) ); - - /* Truncate files that are too large */ - for( i = 0; inf->fileCount > i; i++ ) - { - if( 0 > stat( inf->files[i].name, &sb ) ) - { - if( ENOENT == errno ) - { - continue; - } - tr_err( "Could not stat %s (%s)", - inf->files[i].name, strerror( errno ) ); - break; - } - - if( sb.st_size > ( off_t )inf->files[i].length ) - { - tr_dbg( "truncate %s from %"PRIu64" to %"PRIu64" bytes", - inf->files[i].name, sb.st_size, inf->files[i].length ); - truncate( inf->files[i].name, inf->files[i].length ); - } - } - - /* Check pieces */ - io->slotsUsed = 0; - buf = malloc( inf->pieceSize ); - for( i = 0; i < inf->pieceCount; i++ ) - { - int size, j; - - if( tor->status & TR_STATUS_STOPPING ) - { - io->checkFilesAborted = 1; - break; - } - - if( readSlot( io, i, buf, &size ) ) - { - break; - } - - io->slotsUsed = i + 1; - SHA1( buf, size, hash ); - - for( j = i; j < inf->pieceCount - 1; j++ ) - { - if( !memcmp( hash, &inf->pieces[20*j], SHA_DIGEST_LENGTH ) ) - { - if ( io->pieceSlot[j] > 0 && j == i) - { - /* Only remove double piece when we found one sitting in the right slot */ - - tr_inf( "found piece %d (slot: %d) already on slot %d",j,i,io->pieceSlot[j] ); - io->slotPiece[io->pieceSlot[j]] = -1; - - io->pieceSlot[j] = i; - io->slotPiece[i] = j; - } - else /* We found no double */ - { - io->pieceSlot[j] = i; - io->slotPiece[i] = j; - } - - tr_cpPieceAdd( tor->completion, j ); - break; - } - } - - if( io->slotPiece[i] > -1 ) - { - continue; - } - - /* Special case for the last piece */ - SHA1( buf, tr_pieceSize( inf->pieceCount - 1 ), hash ); - if( !memcmp( hash, &inf->pieces[20 * (inf->pieceCount - 1)], - SHA_DIGEST_LENGTH ) ) - { - io->pieceSlot[inf->pieceCount - 1] = i; - io->slotPiece[i] = inf->pieceCount - 1; - - tr_cpPieceAdd( tor->completion, inf->pieceCount - 1 ); - } - } - free( buf ); - - return 0; -} - -/*********************************************************************** - * closeFiles - **********************************************************************/ -static void closeFiles( tr_io_t * io ) -{ - tr_torrent_t * tor = io->tor; - tr_info_t * inf = &tor->info; - - int i; - - for( i = 0; i < inf->fileCount; i++ ) - { - tr_fdFileClose( tor->destination, inf->files[i].name ); - } -} - -/*********************************************************************** - * readOrWriteBytes - *********************************************************************** - * - **********************************************************************/ -typedef size_t (* iofunc) ( int, void *, size_t ); -static int readOrWriteBytes( tr_io_t * io, uint64_t offset, int size, - uint8_t * buf, int isWrite ) -{ - tr_torrent_t * tor = io->tor; - tr_info_t * inf = &tor->info; - - int piece = offset / inf->pieceSize; - int begin = offset % inf->pieceSize; - int i; - size_t cur; - int file; - iofunc readOrWrite = isWrite ? (iofunc) write : (iofunc) read; - int ret = 0; - - /* Release the torrent lock so the UI can still update itself if - this blocks for a while */ - tr_lockUnlock( &tor->lock ); - - /* We don't ever read or write more than a piece at a time */ - if( tr_pieceSize( piece ) < begin + size ) - { - tr_err( "readOrWriteBytes: trying to write more than a piece" ); - ret = TR_ERROR_ASSERT; - goto cleanup; - } - - /* Find which file we shall start reading/writing in */ - for( i = 0; i < inf->fileCount; i++ ) - { - if( offset < inf->files[i].length ) - { - /* This is the file */ - break; - } - offset -= inf->files[i].length; - } - if( i >= inf->fileCount ) - { - /* Should not happen */ - tr_err( "readOrWriteBytes: offset out of range (%"PRIu64", %d, %d)", - offset, size, isWrite ); - ret = TR_ERROR_ASSERT; - goto cleanup; - } - - while( size > 0 ) - { - /* How much can we put or take with this file */ - if( inf->files[i].length < offset + size ) - { - cur = (int) ( inf->files[i].length - offset ); - } - else - { - cur = size; - } - - if( cur > 0 ) - { - /* Now let's get a descriptor on the file... */ - file = tr_fdFileOpen( tor->destination, inf->files[i].name, - isWrite ); - if( file < 0 ) - { - ret = file; - goto cleanup; - } - - /* seek to the right offset... */ - if( lseek( file, offset, SEEK_SET ) < 0 ) - { - tr_fdFileRelease( file ); - ret = TR_ERROR_IO_OTHER; - goto cleanup; - } - - /* do what we are here to do... */ - if( readOrWrite( file, buf, cur ) != cur ) - { - ret = tr_ioErrorFromErrno(); - tr_fdFileRelease( file ); - goto cleanup; - } - - /* and release the descriptor. */ - tr_fdFileRelease( file ); - } - - /* 'cur' bytes done, 'size - cur' bytes to go with the next file */ - i += 1; - offset = 0; - size -= cur; - buf += cur; - } - -cleanup: - tr_lockLock( &tor->lock ); - - if( ret ) - { - tr_err( "readOrWriteBytes: ret: %d",ret ); - } - return ret; -} - -/*********************************************************************** - * readSlot - *********************************************************************** - * - **********************************************************************/ -static int readOrWriteSlot( tr_io_t * io, int slot, uint8_t * buf, - int * size, int write ) -{ - tr_torrent_t * tor = io->tor; - tr_info_t * inf = &tor->info; - - uint64_t offset = (uint64_t) slot * (uint64_t) inf->pieceSize; - - *size = 0; - if( slot == inf->pieceCount - 1 ) - { - *size = inf->totalSize % inf->pieceSize; - } - if( !*size ) - { - *size = inf->pieceSize; - } - - return readOrWriteBytes( io, offset, *size, buf, write ); -} - -static void invertSlots( tr_io_t * io, int slot1, int slot2 ) -{ - tr_torrent_t * tor = io->tor; - tr_info_t * inf = &tor->info; - - uint8_t * buf1, * buf2; - int piece1, piece2, foo; - - io->writeCount += 2; - io->readCount += 2; - - assert (inf->pieceCount > slot1); - assert (inf->pieceCount > slot2); - assert (slot1 != slot2); - - buf1 = malloc( inf->pieceSize ); - buf2 = malloc( inf->pieceSize ); - - readSlot( io, slot1, buf1, &foo ); - readSlot( io, slot2, buf2, &foo ); - - writeSlot( io, slot2, buf1, &foo ); - writeSlot( io, slot1, buf2, &foo ); - - free( buf1 ); - free( buf2 ); - - piece1 = io->slotPiece[slot1]; - piece2 = io->slotPiece[slot2]; - io->slotPiece[slot1] = piece2; - io->slotPiece[slot2] = piece1; - if( piece1 >= 0 ) - { - io->pieceSlot[piece1] = slot2; - } - if( piece2 >= 0 ) - { - io->pieceSlot[piece2] = slot1; - } -} - -static void moveSlot( tr_io_t * io, int slot1, int slot2 ) -{ - tr_torrent_t * tor = io->tor; - tr_info_t * inf = &tor->info; - - uint8_t * buf1; - int piece, foo; - - io->writeCount += 1; - io->readCount += 1; - - assert (inf->pieceCount > slot1); - assert (inf->pieceCount > slot2); - assert (slot1 != slot2); - - buf1 = malloc( inf->pieceSize ); - - readSlot( io, slot1, buf1, &foo ); /* from slot1 */ - writeSlot( io, slot2, buf1, &foo ); /* to slot2 */ - - free( buf1 ); - - piece = io->slotPiece[slot1]; - - io->slotPiece[slot2] = piece; - io->slotPiece[slot1] = -1; /* mark as free */ - - if( piece >= 0 ) - { - io->pieceSlot[piece] = slot2; - } -} - -static int reorderPieces( tr_io_t * io ) -{ - tr_torrent_t * tor = io->tor; - tr_info_t * inf = &tor->info; - - int i, didInvert, didInvertReturn = 0; - - io->reorderCount += 1; - - /* Try to move pieces to their final places */ - do - { - didInvert = 0; - - for( i = 0; i < inf->pieceCount; i++ ) - { - if( io->pieceSlot[i] < 0 ) - { - /* We haven't started this piece yet */ - continue; - } - if( io->pieceSlot[i] == i ) - { - /* Already in place */ - continue; - } - if( i > io->slotsUsed ) - { - /* File is not big enough yet */ - continue; - } - - /* Move/Invert piece i into slot i */ - - if( io->slotPiece[i] >= 0 ) - { - tr_err( "reorder: invert slot %d and %d (piece %d)", io->pieceSlot[i], i, io->slotPiece[i] ); - invertSlots( io, i, io->pieceSlot[i] ); - } - else - { - tr_inf( "reorder: move slot %d to %d (piece %d)", io->pieceSlot[i], i, io->slotPiece[i] ); - moveSlot( io, io->pieceSlot[i], i ); - if (i == io->slotsUsed) (io->slotsUsed)++; - } - - didInvert = 1; - } - didInvertReturn += didInvert; - } while( didInvert ); - - return didInvertReturn; -} - -static int nextFreeSlotToAppend(tr_io_t * io) -{ - tr_torrent_t * tor = io->tor; - tr_info_t * inf = &tor->info; - - while( io->slotsUsed < ( (inf->pieceCount) - 1 ) && io->pieceSlot[io->slotsUsed] >= 0 ) - { - tr_inf( "slotUsed (%d) has piece in %d !", io->slotsUsed, io->pieceSlot[io->slotsUsed] ); - (io->slotsUsed)++; - } - - return (io->slotsUsed)++; -} - -static void findSlotForPiece( tr_io_t * io, int piece ) -{ - int i; - int freeSlot; - - tr_torrent_t * tor = io->tor; - tr_info_t * inf = &tor->info; - -#if 0 - tr_dbg( "Entering findSlotForPiece" ); - - for( i = 0; i < inf->pieceCount; i++ ) - printf( "%02d ", io->slotPiece[i] ); - printf( "\n" ); - for( i = 0; i < inf->pieceCount; i++ ) - printf( "%02d ", io->pieceSlot[i] ); - printf( "\n" ); -#endif - - /* first we should look for the right slot! */ - -#define PREFERSIZE 0 - - if( piece <= io->slotsUsed ) - { - if( io->slotPiece[piece] >= 0 ) /* if used move it away! */ - { - freeSlot = -1; - - if( PREFERSIZE || io->slotsUsed >= ( inf->pieceCount - 1 ) ) - { - for( i = 0; i < io->slotsUsed; i++ ) - { - if( io->slotPiece[i] < 0 ) - { - freeSlot = i; - break; - } - } - } - - if( freeSlot == -1 ) - freeSlot = nextFreeSlotToAppend(io); - - tr_inf( "move slot %d (piece %d) to %d", piece, io->slotPiece[piece], freeSlot ); - moveSlot( io, piece, freeSlot ); - } - else - { - io->hitCount += 1; - } - - io->pieceSlot[piece] = piece; - io->slotPiece[piece] = piece; - if (piece == io->slotsUsed) (io->slotsUsed)++; - goto reorder; - } - - /* Look for an empty slot somewhere */ - - if( PREFERSIZE || io->slotsUsed >= inf->pieceCount ) - { - for( i = 0; i < io->slotsUsed; i++ ) - { - if( io->slotPiece[i] < 0 ) - { - io->pieceSlot[piece] = i; - io->slotPiece[i] = piece; - goto reorder; - } - } - } - - freeSlot = nextFreeSlotToAppend(io); - - /* No empty slot, extend the file */ - io->pieceSlot[piece] = freeSlot; - io->slotPiece[freeSlot] = piece; - -reorder: - return; - -#if 0 - for( i = 0; i < inf->pieceCount; i++ ) - printf( "%02d ", io->slotPiece[i] ); - printf( "\n" ); - for( i = 0; i < inf->pieceCount; i++ ) - printf( "%02d ", io->pieceSlot[i] ); - printf( "\n" ); - - printf( "Leaving findSlotForPiece\n" ); -#endif -} diff --git a/libtransmission/internal.h b/libtransmission/internal.h index 778b8a35d..27db29293 100644 --- a/libtransmission/internal.h +++ b/libtransmission/internal.h @@ -150,6 +150,13 @@ typedef enum { TR_NET_OK, TR_NET_ERROR, TR_NET_WAIT } tr_tristate_t; #include "http.h" #include "xml.h" +#ifndef TRUE +#define TRUE 1 +#endif +#ifndef FALSE +#define FALSE 0 +#endif + int tr_torrentAddCompact( tr_torrent_t * tor, int from, uint8_t * buf, int count ); int tr_torrentAttachPeer( tr_torrent_t * tor, tr_peer_t * peer ); @@ -168,7 +175,7 @@ struct tr_torrent_s int status; int error; char errorString[128]; - int finished; + int hasChangedState; char * id; char * key; diff --git a/libtransmission/ipcparse.c b/libtransmission/ipcparse.c index 9f680a6a3..3cbac1f5e 100644 --- a/libtransmission/ipcparse.c +++ b/libtransmission/ipcparse.c @@ -705,7 +705,7 @@ ipc_addinfo( benc_val_t * list, int tor, tr_info_t * inf, int types ) } int -ipc_addstat( benc_val_t * list, int tor, tr_info_t * inf, +ipc_addstat( benc_val_t * list, int tor, tr_stat_t * st, int types ) { benc_val_t * dict, * item; @@ -746,7 +746,7 @@ ipc_addstat( benc_val_t * list, int tor, tr_info_t * inf, switch( 1 << ii ) { case IPC_ST_COMPLETED: - tr_bencInitInt( item, st->progress * ( float )inf->totalSize ); + tr_bencInitInt( item, st->downloadedValid ); break; case IPC_ST_DOWNSPEED: tr_bencInitInt( item, st->rateDownload * 1024 ); diff --git a/libtransmission/ipcparse.h b/libtransmission/ipcparse.h index 2a9d1fb20..2e7603521 100644 --- a/libtransmission/ipcparse.h +++ b/libtransmission/ipcparse.h @@ -155,7 +155,7 @@ uint8_t * ipc_mkvers ( size_t * ); uint8_t * ipc_mkgetinfo( struct ipc_info *, size_t *, enum ipc_msg, int64_t, int, const int * ); int ipc_addinfo ( benc_val_t *, int, tr_info_t *, int ); -int ipc_addstat ( benc_val_t *, int, tr_info_t *, tr_stat_t *, int ); +int ipc_addstat ( benc_val_t *, int, tr_stat_t *, int ); /* sets errno to EINVAL on parse error or EPERM for unsupported protocol version */ diff --git a/libtransmission/makemeta.c b/libtransmission/makemeta.c new file mode 100644 index 000000000..d342ea052 --- /dev/null +++ b/libtransmission/makemeta.c @@ -0,0 +1,498 @@ +/* + * This file Copyright (C) 2007 Charles Kerr + * + * This file is licensed by the GPL version 2. Works owned by the + * Transmission project are granted a special exemption to clause 2(b) + * so that the bulk of its code can remain under the MIT license. + * This exemption does not extend to derived works not owned by + * the Transmission project. + */ + +#include +#include +#include +#include +#include +#include /* FILE, snprintf, stderr */ +#include /* malloc, calloc */ + +#include "transmission.h" +#include "internal.h" /* for tr_torrent_t */ +#include "bencode.h" +#include "makemeta.h" +#include "platform.h" /* threads, locks */ +#include "shared.h" /* shared lock */ +#include "version.h" + +/**** +***** +****/ + +struct FileList +{ + struct FileList * next; + char filename[MAX_PATH_LENGTH]; +}; + +static struct FileList* +getFiles( const char * dir, + const char * base, + struct FileList * list ) +{ + int i; + char buf[MAX_PATH_LENGTH]; + struct stat sb; + DIR * odir = NULL; + sb.st_size = 0; + + snprintf( buf, sizeof(buf), "%s"TR_PATH_DELIMITER_STR"%s", dir, base ); + i = stat( buf, &sb ); + if( i ) { + tr_err("makemeta couldn't stat \"%s\"; skipping. (%s)", buf, strerror(errno)); + return list; + } + + if ( S_ISDIR( sb.st_mode ) && (( odir = opendir ( buf ) )) ) + { + struct dirent *d; + for (d = readdir( odir ); d!=NULL; d=readdir( odir ) ) + if( strcmp( d->d_name,"." ) && strcmp( d->d_name,".." ) ) + list = getFiles( buf, d->d_name, list ); + closedir( odir ); + } + else if( S_ISREG( sb.st_mode ) ) + { + struct FileList * node = malloc( sizeof( struct FileList ) ); + snprintf( node->filename, sizeof( node->filename ), "%s", buf ); + node->next = list; + list = node; + } + + return list; +} + +static void +freeFileList( struct FileList * list ) +{ + while( list ) { + struct FileList * tmp = list->next; + free( list ); + list = tmp; + } +} + +static off_t +getFileSize ( const char * filename ) +{ + struct stat sb; + sb.st_size = 0; + stat( filename, &sb ); + return sb.st_size; +} + + +static int +bestPieceSize( uint64_t totalSize ) +{ + const int MiB = 1048576; + const int GiB = totalSize / (uint64_t)1073741824; + + /* almost always best to have a piee size of 512 or 256 kb. + common practice seems to be to bump up to 1MB pieces at + at total size of around 8GiB or so */ + + if( GiB >= 8 ) + return MiB; + + if( GiB >= 1 ) + return MiB / 2; + + return MiB / 4; +} + +/**** +***** +****/ + +static int pstrcmp( const void * va, const void * vb) +{ + const char * a = *(const char**) va; + const char * b = *(const char**) vb; + return strcmp( a, b ); +} + +tr_metainfo_builder_t* +tr_metaInfoBuilderCreate( tr_handle_t * handle, const char * topFile ) +{ + int i; + struct FileList * files; + const struct FileList * walk; + tr_metainfo_builder_t * ret = calloc( 1, sizeof(tr_metainfo_builder_t) ); + ret->top = tr_strdup( topFile ); + ret->handle = handle; + + if (1) { + struct stat sb; + stat( topFile, &sb ); + ret->isSingleFile = !S_ISDIR( sb.st_mode ); + } + + /* build a list of files containing topFile and, + if it's a directory, all of its children */ + if (1) { + char *dir, *base; + char dirbuf[MAX_PATH_LENGTH]; + char basebuf[MAX_PATH_LENGTH]; + strlcpy( dirbuf, topFile, sizeof( dirbuf ) ); + strlcpy( basebuf, topFile, sizeof( basebuf ) ); + dir = dirname( dirbuf ); + base = basename( basebuf ); + files = getFiles( dir, base, NULL ); + } + + for( walk=files; walk!=NULL; walk=walk->next ) + ++ret->fileCount; + ret->files = calloc( ret->fileCount, sizeof(char*) ); + ret->fileLengths = calloc( ret->fileCount, sizeof(uint64_t) ); + + for( i=0, walk=files; walk!=NULL; walk=walk->next, ++i ) + ret->files[i] = tr_strdup( walk->filename ); + + qsort( ret->files, ret->fileCount, sizeof(char*), pstrcmp ); + for( i=0; ifileCount; ++i ) { + ret->fileLengths[i] = getFileSize( ret->files[i] ); + ret->totalSize += ret->fileLengths[i]; + } + + freeFileList( files ); + + ret->pieceSize = bestPieceSize( ret->totalSize ); + ret->pieceCount = (int)( ret->totalSize / ret->pieceSize); + if( ret->totalSize % ret->pieceSize ) + ++ret->pieceCount; + + return ret; +} + +void +tr_metaInfoBuilderFree( tr_metainfo_builder_t * builder ) +{ + if( builder != NULL ) + { + int i; + for( i=0; ifileCount; ++i ) + tr_free( builder->files[i] ); + tr_free( builder->files ); + tr_free( builder->fileLengths ); + tr_free( builder->top ); + tr_free( builder->comment ); + tr_free( builder->announce ); + tr_free( builder->outputFile ); + tr_free( builder ); + } +} + +/**** +***** +****/ + +static uint8_t* +getHashInfo ( tr_metainfo_builder_t * b ) +{ + int fileIndex = 0; + uint8_t *ret = (uint8_t*) malloc ( SHA_DIGEST_LENGTH * b->pieceCount ); + uint8_t *walk = ret; + uint8_t *buf = malloc( b->pieceSize ); + uint64_t totalRemain; + uint64_t off = 0; + FILE * fp; + + b->pieceIndex = 0; + totalRemain = b->totalSize; + fp = fopen( b->files[fileIndex], "rb" ); + while ( totalRemain ) + { + uint8_t *bufptr = buf; + const uint64_t thisPieceSize = MIN( (uint32_t)b->pieceSize, totalRemain ); + uint64_t pieceRemain = thisPieceSize; + + assert( b->pieceIndex < b->pieceCount ); + + while( pieceRemain ) + { + const uint64_t n_this_pass = MIN( (b->fileLengths[fileIndex] - off), pieceRemain ); + fread( bufptr, 1, n_this_pass, fp ); + bufptr += n_this_pass; + off += n_this_pass; + pieceRemain -= n_this_pass; + if( off == b->fileLengths[fileIndex] ) { + off = 0; + fclose( fp ); + fp = NULL; + if( ++fileIndex < b->fileCount ) { + fp = fopen( b->files[fileIndex], "rb" ); + } + } + } + + assert( bufptr-buf == (int)thisPieceSize ); + assert( pieceRemain == 0 ); + SHA1( buf, thisPieceSize, walk ); + walk += SHA_DIGEST_LENGTH; + + if( b->abortFlag ) { + b->failed = 1; + break; + } + + totalRemain -= thisPieceSize; + ++b->pieceIndex; + } + assert( b->abortFlag || (walk-ret == (int)(SHA_DIGEST_LENGTH*b->pieceCount)) ); + assert( b->abortFlag || !totalRemain ); + assert( b->abortFlag || fp == NULL ); + + if( fp != NULL ) + fclose( fp ); + + free( buf ); + return ret; +} + +static void +getFileInfo( const char * topFile, + const char * filename, + benc_val_t * uninitialized_length, + benc_val_t * uninitialized_path ) +{ + benc_val_t *sub; + const char *pch, *prev; + const size_t topLen = strlen(topFile) + 1; /* +1 for '/' */ + int n; + + /* get the file size */ + tr_bencInitInt( uninitialized_length, getFileSize(filename) ); + + /* the path list */ + n = 1; + for( pch=filename+topLen; *pch; ++pch ) + if (*pch == TR_PATH_DELIMITER) + ++n; + tr_bencInit( uninitialized_path, TYPE_LIST ); + tr_bencListReserve( uninitialized_path, n ); + for( prev=pch=filename+topLen; ; ++pch ) + { + char buf[MAX_PATH_LENGTH]; + + if (*pch && *pch!=TR_PATH_DELIMITER ) + continue; + + memcpy( buf, prev, pch-prev ); + buf[pch-prev] = '\0'; + + sub = tr_bencListAdd( uninitialized_path ); + tr_bencInitStrDup( sub, buf ); + + prev = pch + 1; + if (!*pch) + break; + } +} + +static void +makeFilesList( benc_val_t * list, + const tr_metainfo_builder_t * builder ) +{ + int i = 0; + + tr_bencListReserve( list, builder->fileCount ); + + for( i=0; ifileCount; ++i ) + { + benc_val_t * dict = tr_bencListAdd( list ); + benc_val_t *length, *pathVal; + + tr_bencInit( dict, TYPE_DICT ); + tr_bencDictReserve( dict, 2 ); + length = tr_bencDictAdd( dict, "length" ); + pathVal = tr_bencDictAdd( dict, "path" ); + getFileInfo( builder->top, builder->files[i], length, pathVal ); + } +} + +static void +makeInfoDict ( benc_val_t * dict, + tr_metainfo_builder_t * builder ) +{ + uint8_t * pch; + benc_val_t * val; + char base[MAX_PATH_LENGTH]; + + tr_bencDictReserve( dict, 5 ); + + val = tr_bencDictAdd( dict, "name" ); + strlcpy( base, builder->top, sizeof( base ) ); + tr_bencInitStrDup ( val, basename( base ) ); + + val = tr_bencDictAdd( dict, "piece length" ); + tr_bencInitInt( val, builder->pieceSize ); + + pch = getHashInfo( builder ); + val = tr_bencDictAdd( dict, "pieces" ); + tr_bencInitStr( val, pch, SHA_DIGEST_LENGTH * builder->pieceCount, 0 ); + + if ( builder->isSingleFile ) + { + val = tr_bencDictAdd( dict, "length" ); + tr_bencInitInt( val, builder->fileLengths[0] ); + } + else + { + val = tr_bencDictAdd( dict, "files" ); + tr_bencInit( val, TYPE_LIST ); + makeFilesList( val, builder ); + } + + val = tr_bencDictAdd( dict, "private" ); + tr_bencInitInt( val, builder->isPrivate ? 1 : 0 ); +} + +static void tr_realMakeMetaInfo ( tr_metainfo_builder_t * builder ) +{ + int n = 5; + benc_val_t top, *val; + + tr_bencInit ( &top, TYPE_DICT ); + if ( builder->comment && *builder->comment ) ++n; + tr_bencDictReserve( &top, n ); + + val = tr_bencDictAdd( &top, "announce" ); + tr_bencInitStrDup( val, builder->announce ); + + val = tr_bencDictAdd( &top, "created by" ); + tr_bencInitStrDup( val, TR_NAME " " VERSION_STRING ); + + val = tr_bencDictAdd( &top, "creation date" ); + tr_bencInitInt( val, time(0) ); + + val = tr_bencDictAdd( &top, "encoding" ); + tr_bencInitStrDup( val, "UTF-8" ); + + if( builder->comment && *builder->comment ) { + val = tr_bencDictAdd( &top, "comment" ); + tr_bencInitStrDup( val, builder->comment ); + } + + val = tr_bencDictAdd( &top, "info" ); + tr_bencInit( val, TYPE_DICT ); + tr_bencDictReserve( val, 666 ); + makeInfoDict( val, builder ); + + /* save the file */ + if ( !builder->abortFlag ) { + size_t nmemb; + char * pch = tr_bencSaveMalloc( &top, &n ); + FILE * fp = fopen( builder->outputFile, "wb+" ); + nmemb = n; + if( fp == NULL ) + builder->failed = 1; + else if( fwrite( pch, 1, nmemb, fp ) != nmemb ) + builder->failed = 1; + free( pch ); + fclose( fp ); + } + + /* cleanup */ + tr_bencFree( & top ); + builder->failed |= builder->abortFlag; + builder->isDone = 1; +} + +/*** +**** +**** A threaded builder queue +**** +***/ + +static tr_metainfo_builder_t * queue = NULL; + +static int workerIsRunning = 0; + +static tr_thread_t workerThread; + +static tr_lock_t* getQueueLock( tr_handle_t * h ) +{ + static tr_lock_t * lock = NULL; + + tr_sharedLock( h->shared ); + if( lock == NULL ) + { + lock = calloc( 1, sizeof( tr_lock_t ) ); + tr_lockInit( lock ); + } + tr_sharedUnlock( h->shared ); + + return lock; +} + +static void workerFunc( void * user_data ) +{ + tr_handle_t * handle = (tr_handle_t *) user_data; + + for (;;) + { + tr_metainfo_builder_t * builder = NULL; + + /* find the next builder to process */ + tr_lock_t * lock = getQueueLock ( handle ); + tr_lockLock( lock ); + if( queue != NULL ) { + builder = queue; + queue = queue->nextBuilder; + } + tr_lockUnlock( lock ); + + /* if no builders, this worker thread is done */ + if( builder == NULL ) + break; + + tr_realMakeMetaInfo ( builder ); + } + + workerIsRunning = 0; +} + +void +tr_makeMetaInfo( tr_metainfo_builder_t * builder, + const char * outputFile, + const char * announce, + const char * comment, + int isPrivate ) +{ + tr_lock_t * lock; + + builder->abortFlag = 0; + builder->isDone = 0; + builder->announce = tr_strdup( announce ); + builder->comment = tr_strdup( comment ); + builder->isPrivate = isPrivate; + if( outputFile && *outputFile ) + builder->outputFile = tr_strdup( outputFile ); + else { + char out[MAX_PATH_LENGTH]; + snprintf( out, sizeof(out), "%s.torrent", builder->top); + builder->outputFile = tr_strdup( out ); + } + + /* enqueue the builder */ + lock = getQueueLock ( builder->handle ); + tr_lockLock( lock ); + builder->nextBuilder = queue; + queue = builder; + if( !workerIsRunning ) { + workerIsRunning = 1; + tr_threadCreate( &workerThread, workerFunc, builder->handle, "makeMeta" ); + } + tr_lockUnlock( lock ); +} + diff --git a/libtransmission/makemeta.h b/libtransmission/makemeta.h new file mode 100644 index 000000000..d7c1d5016 --- /dev/null +++ b/libtransmission/makemeta.h @@ -0,0 +1,91 @@ +/* + * This file Copyright (C) 2007 Charles Kerr + * + * This file is licensed by the GPL version 2. Works owned by the + * Transmission project are granted a special exemption to clause 2(b) + * so that the bulk of its code can remain under the MIT license. + * This exemption does not extend to derived works not owned by + * the Transmission project. + */ + +#ifndef TR_MAKEMETA_H +#define TR_MAKEMETA_H 1 + +typedef struct tr_metainfo_builder_s +{ + /** + *** These are set by tr_makeMetaInfo() + *** and cleaned up by tr_metaInfoBuilderFree() + **/ + + char * top; + char ** files; + uint64_t * fileLengths; + uint64_t totalSize; + int fileCount; + int pieceSize; + int pieceCount; + int isSingleFile; + tr_handle_t * handle; + + /** + *** These are set inside tr_makeMetaInfo() + *** by copying the arguments passed to it, + *** and cleaned up by tr_metaInfoBuilderFree() + **/ + + char * announce; + char * comment; + char * outputFile; + int isPrivate; + + /** + *** These are set inside tr_makeMetaInfo() so the client + *** can poll periodically to see what the status is. + *** The client can also set abortFlag to nonzero to + *** tell tr_makeMetaInfo() to abort and clean up after itself. + **/ + + int pieceIndex; + int abortFlag; + int isDone; + int failed; /* only meaningful if isDone is set */ + + /** + *** This is an implementation detail. + *** The client should never use these fields. + **/ + + struct tr_metainfo_builder_s * nextBuilder; +} +tr_metainfo_builder_t; + + + + +tr_metainfo_builder_t* +tr_metaInfoBuilderCreate( tr_handle_t * handle, + const char * topFile ); + +void +tr_metaInfoBuilderFree( tr_metainfo_builder_t* ); + +/** + * 'outputFile' if NULL, builder->top + ".torrent" will be used. + * + * This is actually done in a worker thread, not the main thread! + * Otherwise the client's interface would lock up while this runs. + * + * It is the caller's responsibility to poll builder->isDone + * from time to time! When the worker thread sets that flag, + * the caller must pass the builder to tr_metaInfoBuilderFree(). + */ +void +tr_makeMetaInfo( tr_metainfo_builder_t * builder, + const char * outputFile, + const char * announce, + const char * comment, + int isPrivate ); + + +#endif diff --git a/libtransmission/metainfo.c b/libtransmission/metainfo.c index a81da289f..6784407b7 100644 --- a/libtransmission/metainfo.c +++ b/libtransmission/metainfo.c @@ -239,7 +239,13 @@ realparse( tr_info_t * inf, uint8_t * buf, size_t size ) goto fail; } inf->pieceCount = val->val.s.i / SHA_DIGEST_LENGTH; - inf->pieces = (uint8_t *) tr_bencStealStr( val ); + + inf->pieces = calloc ( inf->pieceCount, sizeof(tr_piece_t) ); + + for ( i=0; ipieceCount; ++i ) + { + memcpy (inf->pieces[i].hash, &val->val.s.s[i*SHA_DIGEST_LENGTH], SHA_DIGEST_LENGTH); + } /* TODO add more tests so we don't crash on weird files */ @@ -363,8 +369,8 @@ static int getannounce( tr_info_t * inf, benc_val_t * meta ) if( NULL != val && TYPE_LIST == val->type && 0 < val->val.l.count ) { inf->trackerTiers = 0; - inf->trackerList = calloc( sizeof( inf->trackerList[0] ), - val->val.l.count ); + inf->trackerList = calloc( val->val.l.count, + sizeof( inf->trackerList[0] ) ); /* iterate through the announce-list's tiers */ for( ii = 0; ii < val->val.l.count; ii++ ) @@ -375,7 +381,7 @@ static int getannounce( tr_info_t * inf, benc_val_t * meta ) continue; } subcount = 0; - sublist = calloc( sizeof( sublist[0] ), subval->val.l.count ); + sublist = calloc( subval->val.l.count, sizeof( sublist[0] ) ); /* iterate through the tier's items */ for( jj = 0; jj < subval->val.l.count; jj++ ) @@ -411,7 +417,7 @@ static int getannounce( tr_info_t * inf, benc_val_t * meta ) /* if we skipped some of the tier's items then trim the sublist */ else if( 0 < subcount ) { - inf->trackerList[inf->trackerTiers].list = calloc( sizeof( sublist[0] ), subcount ); + inf->trackerList[inf->trackerTiers].list = calloc( subcount, sizeof( sublist[0] ) ); memcpy( inf->trackerList[inf->trackerTiers].list, sublist, sizeof( sublist[0] ) * subcount ); inf->trackerList[inf->trackerTiers].count = subcount; @@ -436,8 +442,8 @@ static int getannounce( tr_info_t * inf, benc_val_t * meta ) else if( inf->trackerTiers < val->val.l.count ) { swapping = inf->trackerList; - inf->trackerList = calloc( sizeof( inf->trackerList[0] ), - inf->trackerTiers ); + inf->trackerList = calloc( inf->trackerTiers, + sizeof( inf->trackerList[0] ) ); memcpy( inf->trackerList, swapping, sizeof( inf->trackerList[0] ) * inf->trackerTiers ); free( swapping ); @@ -461,12 +467,12 @@ static int getannounce( tr_info_t * inf, benc_val_t * meta ) return 1; } - sublist = calloc( sizeof( sublist[0] ), 1 ); + sublist = calloc( 1, sizeof( sublist[0] ) ); sublist[0].address = address; sublist[0].port = port; sublist[0].announce = announce; sublist[0].scrape = announceToScrape( announce ); - inf->trackerList = calloc( sizeof( inf->trackerList[0] ), 1 ); + inf->trackerList = calloc( 1, sizeof( inf->trackerList[0] ) ); inf->trackerList[0].list = sublist; inf->trackerList[0].count = 1; inf->trackerTiers = 1; diff --git a/libtransmission/peer.c b/libtransmission/peer.c index 020486eb7..0cb8d625d 100644 --- a/libtransmission/peer.c +++ b/libtransmission/peer.c @@ -423,7 +423,8 @@ int tr_peerPulse( tr_peer_t * peer ) } /* Disconnect if seeder and torrent is seeding */ - if( peer->tor->status == TR_STATUS_SEED && peer->progress >= 1.0 ) + if( ( peer->progress >= 1.0 ) + && ( peer->tor->status & (TR_STATUS_SEED|TR_STATUS_DONE) ) ) { return TR_ERROR; } @@ -553,8 +554,9 @@ writeBegin: writeEnd: /* Ask for a block whenever possible */ - if( !tr_cpIsSeeding( tor->completion ) && - !peer->amInterested && tor->peerCount > TR_MAX_PEER_COUNT - 2 ) + if( tr_cpGetStatus( tor->completion ) == TR_CP_INCOMPLETE + && !peer->amInterested + && tor->peerCount > TR_MAX_PEER_COUNT - 2 ) { /* This peer is no use to us, and it seems there are more */ @@ -562,18 +564,41 @@ writeEnd: return TR_ERROR; } - if( peer->amInterested && !peer->peerChoking && !peer->banned ) + if( peer->amInterested + && !peer->peerChoking + && !peer->banned + && peer->inRequestCount < OUR_REQUEST_COUNT ) { - int block; - while( peer->inRequestCount < OUR_REQUEST_COUNT ) - { - block = chooseBlock( tor, peer ); - if( block < 0 ) - { - break; - } - sendRequest( tor, peer, block ); + int i; + int poolSize=0, endgame=0; + int * pool = getPreferredPieces ( tor, peer, &poolSize, &endgame ); + + /* TODO: add some asserts to see if this bitfield is really necessary */ + tr_bitfield_t * blocksAlreadyRequested = tr_bitfieldNew( tor->blockCount ); + for( i=0; iinRequestCount; ++i) { + const tr_request_t * r = &peer->inRequests[i]; + const int block = tr_block( r->index, r->begin ); + tr_bitfieldAdd( blocksAlreadyRequested, block ); } + + for( i=0; iinRequestCountcompletion, piece, &unused) + : tr_cpMissingBlockInPiece ( tor->completion, piece ); + + if( block>=0 && (endgame || !tr_bitfieldHas( blocksAlreadyRequested, block ) ) ) + { + tr_bitfieldAdd( blocksAlreadyRequested, block ); + sendRequest( tor, peer, block ); + } + else ++i; + } + + tr_bitfieldFree( blocksAlreadyRequested ); + free( pool ); } return TR_OK; diff --git a/libtransmission/peermessages.h b/libtransmission/peermessages.h index e815a4756..5c3ddd51f 100644 --- a/libtransmission/peermessages.h +++ b/libtransmission/peermessages.h @@ -285,8 +285,8 @@ static void sendHave( tr_peer_t * peer, int piece ) **********************************************************************/ static void sendBitfield( tr_torrent_t * tor, tr_peer_t * peer ) { - uint8_t * p; - tr_bitfield_t * bitfield; + uint8_t * p; + const tr_bitfield_t * bitfield; bitfield = tr_cpPieceBitfield( tor->completion ); p = getMessagePointer( peer, bitfield->len, PEER_MSG_BITFIELD ); diff --git a/libtransmission/peerparse.h b/libtransmission/peerparse.h index a609cbe5b..d52517450 100644 --- a/libtransmission/peerparse.h +++ b/libtransmission/peerparse.h @@ -173,7 +173,7 @@ static inline int parseBitfield( tr_torrent_t * tor, tr_peer_t * peer, { peer->bitfield = tr_bitfieldNew( inf->pieceCount ); } - assert( bitfieldSize == peer->bitfield->len ); + assert( (unsigned)bitfieldSize == peer->bitfield->len ); memcpy( peer->bitfield->bits, p, bitfieldSize ); peer->pieceCount = 0; diff --git a/libtransmission/peerutils.h b/libtransmission/peerutils.h index 6e227e3cc..aec36007d 100644 --- a/libtransmission/peerutils.h +++ b/libtransmission/peerutils.h @@ -185,16 +185,35 @@ static int checkPeer( tr_peer_t * peer ) return TR_OK; } +static int isPieceInteresting( const tr_torrent_t * tor, + const tr_peer_t * peer, + int piece ) +{ + if( tor->info.pieces[piece].priority == TR_PRI_DND ) /* we don't want it */ + return 0; + + if( !tr_bitfieldHas( peer->bitfield, piece ) ) /* peer doesn't have it */ + return 0; + + if( tr_bitfieldHas( peer->banfield, piece ) ) /* peer is banned for it */ + return 0; + + if( tr_cpPieceIsComplete( tor->completion, piece ) ) /* we already have it */ + return 0; + + return 1; +} + /*********************************************************************** * isInteresting *********************************************************************** - * Returns 1 if 'peer' has at least one piece that we haven't completed, - * or 0 otherwise. + * Returns 1 if 'peer' has at least one piece that we want but + * haven't completed, or 0 otherwise. **********************************************************************/ -static int isInteresting( tr_torrent_t * tor, tr_peer_t * peer ) +static int isInteresting( const tr_torrent_t * tor, const tr_peer_t * peer ) { - int ii; - tr_bitfield_t * bitfield = tr_cpPieceBitfield( tor->completion ); + int i; + const tr_bitfield_t * bitfield = tr_cpPieceBitfield( tor->completion ); if( !peer->bitfield ) { @@ -203,13 +222,10 @@ static int isInteresting( tr_torrent_t * tor, tr_peer_t * peer ) } assert( bitfield->len == peer->bitfield->len ); - for( ii = 0; ii < bitfield->len; ii++ ) - { - if( ( peer->bitfield->bits[ii] & ~(bitfield->bits[ii]) ) & 0xFF ) - { + + for( i=0; iinfo.pieceCount; ++i ) + if( isPieceInteresting( tor, peer, i ) ) return 1; - } - } return 0; } @@ -227,161 +243,103 @@ static void updateInterest( tr_torrent_t * tor, tr_peer_t * peer ) } } -/*********************************************************************** - * chooseBlock - *********************************************************************** - * At this point, we know the peer has at least one block we have an - * interest in. If he has more than one, we choose which one we are - * going to ask first. - * Our main goal is to complete pieces, so we look the pieces which are - * missing less blocks. - **********************************************************************/ -static inline int chooseBlock( tr_torrent_t * tor, tr_peer_t * peer ) +/** utility structure used by getPreferredPieces() and comparePieces() */ +typedef struct { - tr_info_t * inf = &tor->info; + int piece; + tr_priority_t priority; + int missingBlockCount; + int peerCount; +} +PieceCompareData; + +/** utility function used by getPreferredPieces */ +int comparePieces (const void * aIn, const void * bIn) +{ + const PieceCompareData * a = (const PieceCompareData*) aIn; + const PieceCompareData * b = (const PieceCompareData*) bIn; + + /* if one piece has a higher priority, it goes first */ + if (a->priority != b->priority) + return a->priority > b->priority ? -1 : 1; + + /* otherwise if one has fewer missing blocks, it goes first */ + if (a->missingBlockCount != b->missingBlockCount) + return a->missingBlockCount < b->missingBlockCount ? -1 : 1; + + /* otherwise if one has fewer peers, it goes first */ + if (a->peerCount != b->peerCount) + return a->peerCount < b->peerCount ? -1 : 1; + + /* otherwise go with the earlier piece */ + return a->piece - b->piece; +} + +static int* getPreferredPieces( const tr_torrent_t * tor, + const tr_peer_t * peer, + int * pieceCount, + int * endgame ) +{ + const tr_info_t * inf = &tor->info; int i; - int missingBlocks, minMissing; - int poolSize, * pool; - int block, minDownloading; + int poolSize = 0; + int * pool = malloc ( inf->pieceCount * sizeof(int) ); - /* Choose a piece */ - pool = malloc( inf->pieceCount * sizeof( int ) ); - poolSize = 0; - minMissing = tor->blockCount + 1; - for( i = 0; i < inf->pieceCount; i++ ) - { - missingBlocks = tr_cpMissingBlocksForPiece( tor->completion, i ); - if( missingBlocks < 1 ) - { - /* We already have or are downloading all blocks */ - continue; - } - if( !tr_bitfieldHas( peer->bitfield, i ) ) - { - /* The peer doesn't have this piece */ - continue; - } - if( peer->banfield && tr_bitfieldHas( peer->banfield, i ) ) - { - /* The peer is banned for this piece */ - continue; - } + *endgame = 0; - /* We are interested in this piece, remember it */ - if( missingBlocks < minMissing ) - { - minMissing = missingBlocks; - poolSize = 0; - } - if( missingBlocks <= minMissing ) - { - pool[poolSize++] = i; - } + for( i=0; ipieceCount; ++i ) + if( isPieceInteresting( tor, peer, i ) ) + if( tr_cpMissingBlocksForPiece( tor->completion, i ) ) + pool[poolSize++] = i; + + if( !poolSize ) { + *endgame = 1; + for( i=0; ipieceCount; ++i ) + if( isPieceInteresting( tor, peer, i ) ) + pool[poolSize++] = i; } - if( poolSize ) +#if 0 +fprintf (stderr, "old pool: "); +for (i=0; i<15 && i 1 ) { - /* All pieces in 'pool' have 'minMissing' missing blocks. Find - the rarest ones. */ - tr_bitfield_t * bitfield; - int piece; - int min, foo, j; - int * pool2; - int pool2Size; + PieceCompareData * p = malloc ( poolSize * sizeof(PieceCompareData) ); - pool2 = malloc( poolSize * sizeof( int ) ); - pool2Size = 0; - min = TR_MAX_PEER_COUNT + 1; - for( i = 0; i < poolSize; i++ ) + for( i=0; ipeerCount; j++ ) - { - bitfield = tor->peers[j]->bitfield; - if( bitfield && tr_bitfieldHas( bitfield, pool[i] ) ) - { - foo++; - } - } - if( foo < min ) - { - min = foo; - pool2Size = 0; - } - if( foo <= min ) - { - pool2[pool2Size++] = pool[i]; - } - } - free( pool ); + int j; + const int piece = pool[i]; - if( pool2Size < 1 ) - { - /* Shouldn't happen */ - free( pool2 ); - return -1; + p[i].piece = piece; + p[i].priority = inf->pieces[piece].priority; + p[i].missingBlockCount = tr_cpMissingBlocksForPiece( tor->completion, piece ); + p[i].peerCount = 0; + + for( j=0; jpeerCount; ++j ) + if( tr_bitfieldHas( tor->peers[j]->bitfield, piece ) ) + ++p[i].peerCount; } - /* All pieces in pool2 have the same number of missing blocks, - and are availabme from the same number of peers. Pick a - random one */ - piece = pool2[tr_rand(pool2Size)]; - free( pool2 ); + qsort (p, poolSize, sizeof(PieceCompareData), comparePieces); - /* Pick a block in this piece */ - block = tr_cpMissingBlockInPiece( tor->completion, piece ); - goto check; + for( i=0; ipieceCount; i++ ) - { - int downloaders, block2; - if( !tr_bitfieldHas( peer->bitfield, i ) ) - { - /* The peer doesn't have this piece */ - continue; - } - if( peer->banfield && tr_bitfieldHas( peer->banfield, i ) ) - { - /* The peer is banned for this piece */ - continue; - } - if( tr_cpPieceIsComplete( tor->completion, i ) ) - { - /* We already have it */ - continue; - } - block2 = tr_cpMostMissingBlockInPiece( tor->completion, i, &downloaders ); - if( block2 > -1 && downloaders < minDownloading ) - { - block = block2; - minDownloading = downloaders; - } - } - -check: - if( block < 0 ) - { - /* Shouldn't happen */ - return -1; - } - - for( i = 0; i < peer->inRequestCount; i++ ) - { - tr_request_t * r; - r = &peer->inRequests[i]; - if( tr_block( r->index, r->begin ) == block ) - { - /* We are already asking this peer for this block */ - return -1; - } - } - - return block; + *pieceCount = poolSize; + return pool; } diff --git a/libtransmission/torrent.c b/libtransmission/torrent.c index ff877c8c9..062dd251a 100644 --- a/libtransmission/torrent.c +++ b/libtransmission/torrent.c @@ -24,6 +24,7 @@ #include "transmission.h" #include "shared.h" +#define INTERVAL_MSEC 100 /*********************************************************************** * Local prototypes @@ -171,7 +172,7 @@ static tr_torrent_t * torrentRealInit( tr_handle_t * h, tr_torrent_t * tor, tor->id = h->id; tor->key = h->key; tor->azId = h->azId; - tor->finished = 0; + tor->hasChangedState = -1; /* Escaped info hash for HTTP queries */ for( i = 0; i < SHA_DIGEST_LENGTH; i++ ) @@ -215,6 +216,8 @@ static tr_torrent_t * torrentRealInit( tr_handle_t * h, tr_torrent_t * tor, tr_setBindPort( h, TR_DEFAULT_PORT ); } + tr_torrentInitFilePieces( tor ); + return tor; } @@ -240,7 +243,7 @@ void tr_torrentSetFolder( tr_torrent_t * tor, const char * path ) } } -const char * tr_torrentGetFolder( tr_torrent_t * tor ) +char * tr_torrentGetFolder( tr_torrent_t * tor ) { return tor->destination; } @@ -335,8 +338,8 @@ static void torrentReallyStop( tr_torrent_t * tor ) if( tor->tracker ) { - tr_trackerClose( tor->tracker ); - tor->tracker = NULL; + tr_trackerClose( tor->tracker ); + tor->tracker = NULL; } tr_lockLock( &tor->lock ); @@ -374,16 +377,29 @@ void tr_torrentDisablePex( tr_torrent_t * tor, int disable ) tr_lockUnlock( &tor->lock ); } -int tr_getFinished( tr_torrent_t * tor ) +static int tr_didStateChangeTo ( tr_torrent_t * tor, int status ) { - if( tor->finished ) + if( tor->hasChangedState == status ) { - tor->finished = 0; + tor->hasChangedState = -1; return 1; } return 0; } +int tr_getIncomplete( tr_torrent_t * tor ) +{ + return tr_didStateChangeTo( tor, TR_CP_INCOMPLETE ); +} +int tr_getDone( tr_torrent_t * tor ) +{ + return tr_didStateChangeTo( tor, TR_CP_DONE ); +} +int tr_getComplete( tr_torrent_t * tor ) +{ + return tr_didStateChangeTo( tor, TR_CP_COMPLETE ); +} + void tr_manualUpdate( tr_torrent_t * tor ) { int peerCount, new; @@ -459,8 +475,10 @@ tr_stat_t * tr_torrentStat( tr_torrent_t * tor ) } } - s->progress = tr_cpCompletionAsFloat( tor->completion ); - s->left = tr_cpLeftBytes( tor->completion ); + s->percentDone = tr_cpPercentDone( tor->completion ); + s->percentComplete = tr_cpPercentComplete( tor->completion ); + s->cpStatus = tr_cpGetStatus( tor->completion ); + s->left = tr_cpLeftUntilDone( tor->completion ); if( tor->status & TR_STATUS_DOWNLOAD ) { s->rateDownload = tr_rcRate( tor->download ); @@ -493,16 +511,18 @@ tr_stat_t * tr_torrentStat( tr_torrent_t * tor ) s->eta = (float) s->left / s->rateDownload / 1024.0; } - s->downloaded = tor->downloadedCur + tor->downloadedPrev; - s->uploaded = tor->uploadedCur + tor->uploadedPrev; + s->uploaded = tor->uploadedCur + tor->uploadedPrev; + s->downloaded = tor->downloadedCur + tor->downloadedPrev; + s->downloadedValid = tr_cpDownloadedValid( tor->completion ); - if( s->downloaded == 0 && s->progress == 0.0 ) + if( s->downloaded == 0 && s->percentDone == 0.0 ) { s->ratio = TR_RATIO_NA; } else { - s->ratio = (float)s->uploaded / (float)MAX(s->downloaded, inf->totalSize - s->left); + s->ratio = (float)s->uploaded + / (float)MAX(s->downloaded, s->downloadedValid); } tr_lockUnlock( &tor->lock ); @@ -596,53 +616,69 @@ void tr_torrentAvailability( tr_torrent_t * tor, int8_t * tab, int size ) tr_lockUnlock( &tor->lock ); } -float * tr_torrentCompletion( tr_torrent_t * tor ) +size_t +tr_torrentFileBytesCompleted ( const tr_torrent_t * tor, int fileIndex ) { - tr_info_t * inf = &tor->info; - int piece, file; - float * ret, prog, weight; - uint64_t piecemax, piecesize; - uint64_t filestart, fileoff, filelen, blockend, blockused; + const tr_file_t * file = &tor->info.files[fileIndex]; + const int firstBlock = file->offset / tor->blockSize; + const int firstBlockOffset = file->offset % tor->blockSize; + const int lastOffset = file->length ? file->length-1 : 0; + const int lastBlock = (file->offset + lastOffset) / tor->blockSize; + const int lastBlockOffset = (file->offset + lastOffset) % tor->blockSize; + size_t haveBytes = 0; - tr_lockLock( &tor->lock ); + assert( tor != NULL ); + assert( 0<=fileIndex && fileIndexinfo.fileCount ); + assert( file->offset + file->length <= tor->info.totalSize ); + assert( 0<=firstBlock && firstBlockblockCount ); + assert( 0<=lastBlock && lastBlockblockCount ); + assert( firstBlock <= lastBlock ); + assert( tr_blockPiece( firstBlock ) == file->firstPiece ); + assert( tr_blockPiece( lastBlock ) == file->lastPiece ); - ret = calloc( inf->fileCount, sizeof( float ) ); - file = 0; - piecemax = inf->pieceSize; - filestart = 0; - fileoff = 0; - piece = 0; - while( inf->pieceCount > piece ) + if( firstBlock == lastBlock ) { - assert( file < inf->fileCount ); - assert( filestart + fileoff < inf->totalSize ); - filelen = inf->files[file].length; - piecesize = tr_pieceSize( piece ); - blockend = MIN( filestart + filelen, piecemax * piece + piecesize ); - blockused = blockend - ( filestart + fileoff ); - weight = ( filelen ? ( float )blockused / ( float )filelen : 1.0 ); - prog = tr_cpPercentBlocksInPiece( tor->completion, piece ); - ret[file] += prog * weight; - fileoff += blockused; - assert( -0.1 < prog && 1.1 > prog ); - assert( -0.1 < weight && 1.1 > weight ); - if( fileoff == filelen ) - { - ret[file] = MIN( 1.0, ret[file] ); - ret[file] = MAX( 0.0, ret[file] ); - filestart += fileoff; - fileoff = 0; - file++; - } - if( filestart + fileoff >= piecemax * piece + piecesize ) - { - piece++; - } + if( tr_cpBlockIsComplete( tor->completion, firstBlock ) ) + haveBytes += lastBlockOffset + 1 - firstBlockOffset; + } + else + { + int i; + + if( tr_cpBlockIsComplete( tor->completion, firstBlock ) ) + haveBytes += tor->blockSize - firstBlockOffset; + + for( i=firstBlock+1; icompletion, i ) ) + haveBytes += tor->blockSize; + + if( tr_cpBlockIsComplete( tor->completion, lastBlock ) ) + haveBytes += lastBlockOffset + 1; } + return haveBytes; +} + +float +tr_torrentFileCompletion ( const tr_torrent_t * tor, int fileIndex ) +{ + const size_t c = tr_torrentFileBytesCompleted ( tor, fileIndex ); + return (float)c / tor->info.files[fileIndex].length; +} + +float* +tr_torrentCompletion( tr_torrent_t * tor ) +{ + int i; + float * f; + + tr_lockLock( &tor->lock ); + f = calloc ( tor->info.fileCount, sizeof( float ) ); + for( i=0; iinfo.fileCount; ++i ) + f[i] = tr_torrentFileCompletion ( tor, i ); tr_lockUnlock( &tor->lock ); - return ret; + return f; } void tr_torrentAmountFinished( tr_torrent_t * tor, float * tab, int size ) @@ -917,30 +953,43 @@ static void downloadLoop( void * _tor ) tr_torrent_t * tor = _tor; int i, ret; int peerCount, used; + cp_status_t cpState, cpPrevState; uint8_t * peerCompact; tr_peer_t * peer; - tr_dbg ("torrent %s has its own thread", tor->info.name); tr_lockLock( &tor->lock ); - tor->status = tr_cpIsSeeding( tor->completion ) ? - TR_STATUS_SEED : TR_STATUS_DOWNLOAD; + cpState = cpPrevState = tr_cpGetStatus( tor->completion ); + switch( cpState ) { + case TR_CP_COMPLETE: tor->status = TR_STATUS_SEED; break; + case TR_CP_DONE: tor->status = TR_STATUS_DONE; break; + case TR_CP_INCOMPLETE: tor->status = TR_STATUS_DOWNLOAD; break; + } while( !tor->die ) { tr_lockUnlock( &tor->lock ); - tr_wait( 20 ); + tr_wait( INTERVAL_MSEC ); tr_lockLock( &tor->lock ); - /* Are we finished ? */ - if( ( tor->status & TR_STATUS_DOWNLOAD ) && - tr_cpIsSeeding( tor->completion ) ) + cpState = tr_cpGetStatus( tor->completion ); + + if( cpState != cpPrevState ) { - /* Done */ - tor->status = TR_STATUS_SEED; - tor->finished = 1; - tr_trackerCompleted( tor->tracker ); + switch( cpState ) { + case TR_CP_COMPLETE: tor->status = TR_STATUS_SEED; break; + case TR_CP_DONE: tor->status = TR_STATUS_DONE; break; + case TR_CP_INCOMPLETE: tor->status = TR_STATUS_DOWNLOAD; break; + } + + tor->hasChangedState = cpState; + + if( cpState == TR_CP_COMPLETE ) + tr_trackerCompleted( tor->tracker ); + tr_ioSync( tor->io ); + + cpPrevState = cpState; } /* Try to get new peers or to send a message to the tracker */ @@ -1016,3 +1065,123 @@ static void downloadLoop( void * _tor ) tor->status = TR_STATUS_STOPPED; } +/*** +**** +**** File prioritization +**** +***/ + +static int +getBytePiece( const tr_info_t * info, uint64_t byteOffset ) +{ + assert( info != NULL ); + assert( info->pieceSize != 0 ); + + return byteOffset / info->pieceSize; +} + +static void +initFilePieces ( tr_info_t * info, int fileIndex ) +{ + tr_file_t * file = &info->files[fileIndex]; + uint64_t firstByte, lastByte; + + assert( info != NULL ); + assert( 0<=fileIndex && fileIndexfileCount ); + + file = &info->files[fileIndex]; + firstByte = file->offset; + lastByte = firstByte + (file->length ? file->length-1 : 0); + file->firstPiece = getBytePiece( info, firstByte ); + file->lastPiece = getBytePiece( info, lastByte ); + tr_dbg( "file #%d is in pieces [%d...%d] (%s)", fileIndex, file->firstPiece, file->lastPiece, file->name ); +} + +static tr_priority_t +calculatePiecePriority ( const tr_torrent_t * tor, + int piece ) +{ + int i; + tr_priority_t priority = TR_PRI_DND; + + for( i=0; iinfo.fileCount; ++i ) + { + const tr_file_t * file = &tor->info.files[i]; + if ( file->firstPiece <= piece + && file->lastPiece >= piece + && file->priority > priority) + priority = file->priority; + } + + return priority; +} + +void +tr_torrentInitFilePieces( tr_torrent_t * tor ) +{ + int i; + uint64_t offset = 0; + + assert( tor != NULL ); + + for( i=0; iinfo.fileCount; ++i ) { + tor->info.files[i].offset = offset; + offset += tor->info.files[i].length; + initFilePieces( &tor->info, i ); + } + + for( i=0; iinfo.pieceCount; ++i ) + tor->info.pieces[i].priority = calculatePiecePriority( tor, i ); +} + +void +tr_torrentSetFilePriority( tr_torrent_t * tor, + int fileIndex, + tr_priority_t priority ) +{ + int i; + tr_file_t * file; + + assert( tor != NULL ); + assert( 0<=fileIndex && fileIndexinfo.fileCount ); + assert( priority==TR_PRI_LOW || priority==TR_PRI_NORMAL + || priority==TR_PRI_HIGH || priority==TR_PRI_DND ); + + file = &tor->info.files[fileIndex]; + file->priority = priority; + for( i=file->firstPiece; i<=file->lastPiece; ++i ) + tor->info.pieces[i].priority = calculatePiecePriority( tor, i ); + + tr_dbg ( "Setting file #%d (pieces %d-%d) priority to %d (%s)", + fileIndex, file->firstPiece, file->lastPiece, + priority, tor->info.files[fileIndex].name ); +} + +tr_priority_t +tr_torrentGetFilePriority( const tr_torrent_t * tor, int file ) +{ + assert( tor != NULL ); + assert( 0<=file && fileinfo.fileCount ); + + return tor->info.files[file].priority; +} + + +void +tr_torrentSetFilePriorities( tr_torrent_t * tor, + const tr_priority_t * filePriorities ) +{ + int i; + for( i=0; iinfo.pieceCount; ++i ) + tr_torrentSetFilePriority( tor, i, filePriorities[i] ); +} + +tr_priority_t* +tr_torrentGetFilePriorities( const tr_torrent_t * tor ) +{ + int i; + tr_priority_t * p = malloc( tor->info.fileCount * sizeof(tr_priority_t) ); + for( i=0; iinfo.fileCount; ++i ) + p[i] = tor->info.files[i].priority; + return p; +} diff --git a/libtransmission/tracker.c b/libtransmission/tracker.c index ca100f6e1..d3835a34d 100644 --- a/libtransmission/tracker.c +++ b/libtransmission/tracker.c @@ -236,7 +236,8 @@ static int shouldConnect( tr_tracker_t * tc ) /* If there is quite a lot of people on this torrent, stress the tracker a bit until we get a decent number of peers */ - if( tc->hasManyPeers && !tr_cpIsSeeding( tor->completion ) ) + if( tc->hasManyPeers && + (tr_cpGetStatus ( tor->completion ) == TR_CP_INCOMPLETE )) { /* reannounce in 10 seconds if we have less than 5 peers */ if( tor->peerCount < 5 ) @@ -558,7 +559,7 @@ static tr_http_t * getQuery( tr_tracker_t * tc ) } start = ( strchr( tcInf->announce, '?' ) ? '&' : '?' ); - left = tr_cpLeftBytes( tor->completion ); + left = tr_cpLeftUntilComplete( tor->completion ); return tr_httpClient( TR_HTTP_GET, tcInf->address, tcInf->port, "%s%c" diff --git a/libtransmission/transmission.h b/libtransmission/transmission.h index 45126d9f5..bb896d1b0 100644 --- a/libtransmission/transmission.h +++ b/libtransmission/transmission.h @@ -52,6 +52,9 @@ extern "C" { #define INET_ADDRSTRLEN 16 #endif +#define TR_PATH_DELIMITER '/' +#define TR_PATH_DELIMITER_STR "/" + #define TR_DEFAULT_PORT 9090 #define TR_PEER_FROM__MAX 4 @@ -177,6 +180,7 @@ void tr_setGlobalDownloadLimit( tr_handle_t *, int ); **********************************************************************/ int tr_torrentCount( tr_handle_t * h ); + /*********************************************************************** * tr_torrentIterate *********************************************************************** @@ -190,6 +194,44 @@ void tr_setUseCustomLimit( tr_torrent_t * tor, int limit ); void tr_setUploadLimit( tr_torrent_t * tor, int limit ); void tr_setDownloadLimit( tr_torrent_t * tor, int limit ); +/*********************************************************************** + * Torrent Priorities + **********************************************************************/ + +enum +{ + TR_PRI_DND = -2, /* Do Not Download */ + TR_PRI_LOW = -1, + TR_PRI_NORMAL = 0, /* since NORMAL is 0, memset initializes nicely */ + TR_PRI_HIGH = 1 +}; + +typedef int8_t tr_priority_t; + +void tr_torrentInitFilePieces( tr_torrent_t * tor ); + +/* priorities should be an array of tor->info.fileCount bytes, + * each holding a value of TR_PRI_NORMAL, _HIGH, _LOW, or _DND. */ +void tr_torrentSetFilePriorities ( tr_torrent_t *, const tr_priority_t * priorities ); + +/* single-file form of tr_torrentPrioritizeFiles. + * priority must be one of TR_PRI_NORMAL, _HIGH, _LOW, or _DND */ +void tr_torrentSetFilePriority( tr_torrent_t *, int file, tr_priority_t priority ); + +/* returns a malloc()ed array of tor->info.fileCount items, + * each holding a value of TR_PRI_NORMAL, _HIGH, _LOW, or _DND. + free the array when done. */ +tr_priority_t* tr_torrentGetFilePriorities( const tr_torrent_t * ); + +/* single-file form of tr_torrentGetFilePriorities. + * returns one of TR_PRI_NORMAL, _HIGH, _LOW, or _DND */ +tr_priority_t tr_torrentGetFilePriority( const tr_torrent_t *, int file ); + +/* returns the priority for the specified piece, + * as ranked by the number of files of various priorities in that piece. + * a zero priority means DND */ +tr_priority_t tr_torrentGetPiecePriority( const tr_torrent_t *, int pieceIndex ); + /*********************************************************************** * tr_torrentRates *********************************************************************** @@ -270,7 +312,7 @@ tr_info_t * tr_torrentInfo( tr_torrent_t * ); int tr_torrentScrape( tr_torrent_t *, int * s, int * l, int * d ); void tr_torrentSetFolder( tr_torrent_t *, const char * ); -const char * tr_torrentGetFolder( tr_torrent_t * ); +char * tr_torrentGetFolder( tr_torrent_t * ); int tr_torrentDuplicateDownload( tr_torrent_t * tor ); @@ -296,12 +338,15 @@ void tr_torrentStart( tr_torrent_t * ); void tr_torrentStop( tr_torrent_t * ); /*********************************************************************** - * tr_getFinished + * tr_getComplete, tr_getIncomplete and tr_getPartial *********************************************************************** - * The first call after a torrent is completed returns 1. Returns 0 + * The first call after a torrent changed state returns 1. Returns 0 * in other cases. **********************************************************************/ -int tr_getFinished( tr_torrent_t * ); +int tr_getIncomplete( tr_torrent_t * tor ); +int tr_getDone( tr_torrent_t * tor ); +int tr_getComplete( tr_torrent_t * tor ); + /*********************************************************************** * tr_manualUpdate @@ -349,7 +394,12 @@ void tr_torrentAmountFinished( tr_torrent_t * tor, float * tab, int size ); * array of floats the same size and order as in tr_info_t. Free the * array when done. **********************************************************************/ -float * tr_torrentCompletion( tr_torrent_t * tor ); +float * tr_torrentCompletion( tr_torrent_t * ); + +float tr_torrentFileCompletion( const tr_torrent_t *, int fileIndex ); + +size_t tr_torrentFileBytesCompleted( const tr_torrent_t *, int fileIndex ); + /*********************************************************************** * tr_torrentRemoveSaved @@ -372,12 +422,25 @@ void tr_torrentClose( tr_torrent_t * ); /*********************************************************************** * tr_info_s **********************************************************************/ + typedef struct tr_file_s { uint64_t length; /* Length of the file, in bytes */ char name[MAX_PATH_LENGTH]; /* Path to the file */ + int8_t priority; /* TR_PRI_HIGH, _NORMAL, _LOW, or _DND */ + int firstPiece; /* We need pieces [firstPiece... */ + int lastPiece; /* ...lastPiece] to dl this file */ + uint64_t offset; /* file begins at the torrent's nth byte */ } tr_file_t; + +typedef struct tr_piece_s +{ + uint8_t hash[SHA_DIGEST_LENGTH]; /* pieces hash */ + int8_t priority; /* TR_PRI_HIGH, _NORMAL, _LOW, or _DND */ +} +tr_piece_t; + struct tr_info_s { /* Path to torrent */ @@ -410,7 +473,7 @@ struct tr_info_s int pieceSize; int pieceCount; uint64_t totalSize; - uint8_t * pieces; + tr_piece_t * pieces; /* Files info */ int multifile; @@ -418,23 +481,35 @@ struct tr_info_s tr_file_t * files; }; +typedef enum +{ + TR_CP_COMPLETE, /* has every piece */ + TR_CP_DONE, /* has all the pieces but the DND ones */ + TR_CP_INCOMPLETE /* doesn't have all the desired pieces */ +} +cp_status_t; + /*********************************************************************** * tr_stat_s **********************************************************************/ struct tr_stat_s { -#define TR_STATUS_CHECK_WAIT 0x001 /* Waiting in queue to check files */ -#define TR_STATUS_CHECK 0x002 /* Checking files */ -#define TR_STATUS_DOWNLOAD 0x004 /* Downloading */ -#define TR_STATUS_SEED 0x008 /* Seeding */ -#define TR_STATUS_STOPPING 0x010 /* Sending 'stopped' to the tracker */ -#define TR_STATUS_STOPPED 0x020 /* Sent 'stopped' but thread still - running (for internal use only) */ -#define TR_STATUS_PAUSE 0x040 /* Paused */ +#define TR_STATUS_CHECK_WAIT (1<<0) /* Waiting in queue to check files */ +#define TR_STATUS_CHECK (1<<1) /* Checking files */ +#define TR_STATUS_DOWNLOAD (1<<2) /* Downloading */ +#define TR_STATUS_DONE (1<<3) /* not at 100% so can't tell the tracker + we're a seeder, but due to DND files + there's nothing we want right now */ +#define TR_STATUS_SEED (1<<4) /* Seeding */ +#define TR_STATUS_STOPPING (1<<5) /* Sending 'stopped' to the tracker */ +#define TR_STATUS_STOPPED (1<<6) /* Sent 'stopped' but thread still + running (for internal use only) */ +#define TR_STATUS_PAUSE (1<<7) /* Paused */ -#define TR_STATUS_ACTIVE (TR_STATUS_CHECK_WAIT|TR_STATUS_CHECK|TR_STATUS_DOWNLOAD|TR_STATUS_SEED) +#define TR_STATUS_ACTIVE (TR_STATUS_CHECK_WAIT|TR_STATUS_CHECK|TR_STATUS_DOWNLOAD|TR_STATUS_DONE|TR_STATUS_SEED) #define TR_STATUS_INACTIVE (TR_STATUS_STOPPING|TR_STATUS_STOPPED|TR_STATUS_PAUSE) int status; + cp_status_t cpStatus; int error; char errorString[128]; @@ -442,7 +517,8 @@ struct tr_stat_s tr_tracker_info_t * tracker; - float progress; + float percentComplete; + float percentDone; float rateDownload; float rateUpload; int eta; @@ -456,6 +532,7 @@ struct tr_stat_s uint64_t left; uint64_t downloaded; + uint64_t downloadedValid; uint64_t uploaded; float swarmspeed; diff --git a/libtransmission/utils.c b/libtransmission/utils.c index e61a50e54..8e48075cc 100644 --- a/libtransmission/utils.c +++ b/libtransmission/utils.c @@ -22,6 +22,7 @@ * DEALINGS IN THE SOFTWARE. *****************************************************************************/ +#include #include "transmission.h" #define SPRINTF_BUFSIZE 100 @@ -161,32 +162,22 @@ int tr_rand( int sup ) return rand() % sup; } -void * tr_memmem( const void *vbig, size_t big_len, - const void *vlittle, size_t little_len ) + +void* +tr_memmem( const void* haystack, size_t hl, + const void* needle, size_t nl) { - const char *big = vbig; - const char *little = vlittle; - size_t ii, jj; + const char *walk, *end; - if( 0 == big_len || 0 == little_len ) - { + if( !nl ) + return (void*) haystack; + + if( hl < nl ) return NULL; - } - for( ii = 0; ii + little_len <= big_len; ii++ ) - { - for( jj = 0; jj < little_len; jj++ ) - { - if( big[ii + jj] != little[jj] ) - { - break; - } - } - if( jj == little_len ) - { - return (char*)big + ii; - } - } + for (walk=(const char*)haystack, end=walk+hl-nl; walk!=end; ++walk) + if( !memcmp( walk, needle, nl ) ) + return (void*) walk; return NULL; } @@ -242,44 +233,20 @@ int tr_mkdir( char * path ) return 0; } -#define UPPER( cc ) \ - ( 'a' <= (cc) && 'z' >= (cc) ? (cc) - ( 'a' - 'A' ) : (cc) ) - -int tr_strncasecmp( const char * first, const char * second, int len ) +int +tr_strncasecmp( const char * s1, const char * s2, size_t n ) { - int ii; - char firstchar, secondchar; + if ( !n ) + return 0; - if( 0 > len ) - { - len = strlen( first ); - ii = strlen( second ); - len = MIN( len, ii ); + while( n-- != 0 && tolower( *s1 ) == tolower( *s2 ) ) { + if( !n || !*s1 || !*s2 ) + break; + ++s1; + ++s2; } - for( ii = 0; ii < len; ii++ ) - { - if( first[ii] != second[ii] ) - { - firstchar = UPPER( first[ii] ); - secondchar = UPPER( second[ii] ); - if( firstchar > secondchar ) - { - return 1; - } - else if( firstchar < secondchar ) - { - return -1; - } - } - if( '\0' == first[ii] ) - { - /* if first[ii] is '\0' then second[ii] is too */ - return 0; - } - } - - return 0; + return tolower(*(unsigned char *) s1) - tolower(*(unsigned char *) s2); } int tr_sprintf( char ** buf, int * used, int * max, const char * format, ... ) @@ -362,20 +329,20 @@ tr_dupstr( const char * base, int len ) int tr_ioErrorFromErrno( void ) { - if( EACCES == errno || EROFS == errno ) + switch( errno ) { - return TR_ERROR_IO_PERMISSIONS; + case EACCES: + case EROFS: + return TR_ERROR_IO_PERMISSIONS; + case ENOSPC: + return TR_ERROR_IO_SPACE; + case EMFILE: + case EFBIG: + return TR_ERROR_IO_RESOURCES; + default: + tr_dbg( "generic i/o errno from errno: %s", strerror( errno ) ); + return TR_ERROR_IO_OTHER; } - else if( ENOSPC == errno ) - { - return TR_ERROR_IO_SPACE; - } - else if( EMFILE == errno || EFBIG == errno ) - { - return TR_ERROR_IO_RESOURCES; - } - tr_dbg( "generic i/o errno from errno: %s", strerror( errno ) ); - return TR_ERROR_IO_OTHER; } char * @@ -405,3 +372,192 @@ tr_errorString( int code ) return "Unknown error"; } +/**** +***** +****/ + +char* tr_strdup( const char * in ) +{ + char * out = NULL; + if( in != NULL ) + { + const size_t len = strlen( in ); + out = malloc( len + 1 ); + memcpy( out, in, len ); + out[len] = '\0'; + } + return out; +} + +void* +tr_malloc( size_t size ) +{ + return size ? malloc( size ) : NULL; +} + +void tr_free( void * p ) +{ + if( p ) + free( p ); +} + +/**** +***** +****/ + +/* note that the argument is how many bits are needed, not bytes */ +tr_bitfield_t* +tr_bitfieldNew( size_t bitcount ) +{ + tr_bitfield_t * ret = calloc( 1, sizeof(tr_bitfield_t) ); + if( NULL == ret ) + return NULL; + + ret->len = ( bitcount + 7u ) / 8u; + ret->bits = calloc( ret->len, 1 ); + if( NULL == ret->bits ) { + free( ret ); + return NULL; + } + + return ret; +} + +tr_bitfield_t* +tr_bitfieldDup( const tr_bitfield_t * in ) +{ + tr_bitfield_t * ret = calloc( 1, sizeof(tr_bitfield_t) ); + ret->len = in->len; + ret->bits = malloc( ret->len ); + memcpy( ret->bits, in->bits, ret->len ); + return ret; +} + +void tr_bitfieldFree( tr_bitfield_t * bitfield ) +{ + if( bitfield ) + { + free( bitfield->bits ); + free( bitfield ); + } +} + +void +tr_bitfieldClear( tr_bitfield_t * bitfield ) +{ + memset( bitfield->bits, 0, bitfield->len ); +} + +int +tr_bitfieldIsEmpty( const tr_bitfield_t * bitfield ) +{ + unsigned int i; + + for( i=0; ilen; ++i ) + if( *bitfield->bits ) + return 0; + + return 1; +} + +int +tr_bitfieldHas( const tr_bitfield_t * bitfield, + size_t bit ) +{ + if ( bitfield == NULL ) return 0; + assert( bit / 8u < bitfield->len ); + return ( bitfield->bits[ bit / 8u ] & ( 1 << ( 7 - ( bit % 8 ) ) ) ); +} + +void +tr_bitfieldAdd( tr_bitfield_t * bitfield, size_t bit ) +{ + assert( bit / 8u < bitfield->len ); + bitfield->bits[ bit / 8u ] |= ( 1u << ( 7u - ( bit % 8u ) ) ); +} + +void +tr_bitfieldAddRange( tr_bitfield_t * bitfield, + size_t first, + size_t last ) +{ + /* TODO: there are faster ways to do this */ + unsigned int i; + for( i=first; i<=last; ++i ) + tr_bitfieldAdd( bitfield, i ); +} + +void +tr_bitfieldRem( tr_bitfield_t * bitfield, + size_t bit ) +{ + assert( bit / 8u < bitfield->len ); + bitfield->bits[ bit / 8u ] &= ~( 1u << ( 7u - ( bit % 8u ) ) ); +} + +void +tr_bitfieldRemRange ( tr_bitfield_t * b, + size_t first, + size_t last ) +{ + /* TODO: there are faster ways to do this */ + unsigned int i; + for( i=first; i<=last; ++i ) + tr_bitfieldRem( b, i ); +} + +tr_bitfield_t* +tr_bitfieldNegate( tr_bitfield_t * b ) +{ + uint8_t *it; + const uint8_t *end; + + for( it=b->bits, end=it+b->len; it!=end; ++it ) + *it = ~*it; + + return b; +} + +tr_bitfield_t* +tr_bitfieldAnd( tr_bitfield_t * a, const tr_bitfield_t * b ) +{ + uint8_t *ait; + const uint8_t *aend, *bit; + + assert( a->len == b->len ); + + for( ait=a->bits, bit=b->bits, aend=ait+a->len; ait!=aend; ++ait, ++bit ) + *ait &= *bit; + + return a; +} + +size_t +tr_bitfieldCountTrueBits( const tr_bitfield_t* b ) +{ + size_t ret = 0; + const uint8_t *it, *end; + static const int trueBitCount[512] = { + 0,1,1,2,1,2,2,3,1,2,2,3,2,3,3,4,1,2,2,3,2,3,3,4,2,3,3,4,3,4,4,5, + 1,2,2,3,2,3,3,4,2,3,3,4,3,4,4,5,2,3,3,4,3,4,4,5,3,4,4,5,4,5,5,6, + 1,2,2,3,2,3,3,4,2,3,3,4,3,4,4,5,2,3,3,4,3,4,4,5,3,4,4,5,4,5,5,6, + 2,3,3,4,3,4,4,5,3,4,4,5,4,5,5,6,3,4,4,5,4,5,5,6,4,5,5,6,5,6,6,7, + 1,2,2,3,2,3,3,4,2,3,3,4,3,4,4,5,2,3,3,4,3,4,4,5,3,4,4,5,4,5,5,6, + 2,3,3,4,3,4,4,5,3,4,4,5,4,5,5,6,3,4,4,5,4,5,5,6,4,5,5,6,5,6,6,7, + 2,3,3,4,3,4,4,5,3,4,4,5,4,5,5,6,3,4,4,5,4,5,5,6,4,5,5,6,5,6,6,7, + 3,4,4,5,4,5,5,6,4,5,5,6,5,6,6,7,4,5,5,6,5,6,6,7,5,6,6,7,6,7,7,8, + 1,2,2,3,2,3,3,4,2,3,3,4,3,4,4,5,2,3,3,4,3,4,4,5,3,4,4,5,4,5,5,6, + 2,3,3,4,3,4,4,5,3,4,4,5,4,5,5,6,3,4,4,5,4,5,5,6,4,5,5,6,5,6,6,7, + 2,3,3,4,3,4,4,5,3,4,4,5,4,5,5,6,3,4,4,5,4,5,5,6,4,5,5,6,5,6,6,7, + 3,4,4,5,4,5,5,6,4,5,5,6,5,6,6,7,4,5,5,6,5,6,6,7,5,6,6,7,6,7,7,8, + 2,3,3,4,3,4,4,5,3,4,4,5,4,5,5,6,3,4,4,5,4,5,5,6,4,5,5,6,5,6,6,7, + 3,4,4,5,4,5,5,6,4,5,5,6,5,6,6,7,4,5,5,6,5,6,6,7,5,6,6,7,6,7,7,8, + 3,4,4,5,4,5,5,6,4,5,5,6,5,6,6,7,4,5,5,6,5,6,6,7,5,6,6,7,6,7,7,8, + 4,5,5,6,5,6,6,7,5,6,6,7,6,7,7,8,5,6,6,7,6,7,7,8,6,7,7,8,7,8,8,9 + }; + + for( it=b->bits, end=it+b->len; it!=end; ++it ) + ret += trueBitCount[*it]; + + return ret; +} diff --git a/libtransmission/utils.h b/libtransmission/utils.h index 50b13b331..4c2d9afb7 100644 --- a/libtransmission/utils.h +++ b/libtransmission/utils.h @@ -49,8 +49,8 @@ int tr_mkdir( char * path ); *********************************************************************** * A case-insensitive strncmp() **********************************************************************/ -#define tr_strcasecmp( ff, ss ) ( tr_strncasecmp( (ff), (ss), -1 ) ) -int tr_strncasecmp( const char * first, const char * second, int len ); +#define tr_strcasecmp( ff, ss ) ( tr_strncasecmp( (ff), (ss), ULONG_MAX ) ) +int tr_strncasecmp( const char * first, const char * second, size_t len ); /*********************************************************************** * tr_sprintf @@ -103,70 +103,6 @@ static inline void tr_wait( uint64_t delay ) #endif } -struct tr_bitfield_s -{ - uint8_t * bits; - int len; -}; - -/* note that the argument is how many bits are needed, not bytes */ -static inline tr_bitfield_t * tr_bitfieldNew( int bitcount ) -{ - tr_bitfield_t * ret; - - ret = calloc( 1, sizeof *ret ); - if( NULL == ret ) - { - return NULL; - } - ret->len = ( bitcount + 7 ) / 8; - ret->bits = calloc( ret->len, 1 ); - if( NULL == ret->bits ) - { - free( ret ); - return NULL; - } - - return ret; -} - -static inline void tr_bitfieldFree( tr_bitfield_t * bitfield ) -{ - if( bitfield ) - { - free( bitfield->bits ); - free( bitfield ); - } -} - -static inline void tr_bitfieldClear( tr_bitfield_t * bitfield ) -{ - memset( bitfield->bits, 0, bitfield->len ); -} - -/*********************************************************************** - * tr_bitfieldHas - **********************************************************************/ -static inline int tr_bitfieldHas( tr_bitfield_t * bitfield, int piece ) -{ - assert( piece / 8 < bitfield->len ); - return ( bitfield->bits[ piece / 8 ] & ( 1 << ( 7 - ( piece % 8 ) ) ) ); -} - -/*********************************************************************** - * tr_bitfieldAdd - **********************************************************************/ -static inline void tr_bitfieldAdd( tr_bitfield_t * bitfield, int piece ) -{ - assert( piece / 8 < bitfield->len ); - bitfield->bits[ piece / 8 ] |= ( 1 << ( 7 - ( piece % 8 ) ) ); -} - -static inline void tr_bitfieldRem( tr_bitfield_t * bitfield, int piece ) -{ - assert( piece / 8 < bitfield->len ); - bitfield->bits[ piece / 8 ] &= ~( 1 << ( 7 - ( piece % 8 ) ) ); -} #define tr_blockPiece(a) _tr_blockPiece(tor,a) static inline int _tr_blockPiece( const tr_torrent_t * tor, int block ) @@ -237,4 +173,43 @@ static inline int _tr_block( const tr_torrent_t * tor, int index, int begin ) begin / tor->blockSize; } +/*** +**** +***/ + +char* tr_strdup( const char * pch ); + +void* tr_malloc( size_t ); + +void tr_free( void* ); + +/*** +**** +***/ + +struct tr_bitfield_s +{ + uint8_t * bits; + size_t len; +}; + +tr_bitfield_t* tr_bitfieldNew( size_t bitcount ); +tr_bitfield_t* tr_bitfieldDup( const tr_bitfield_t* ); +void tr_bitfieldFree( tr_bitfield_t*); + +void tr_bitfieldClear( tr_bitfield_t* ); +void tr_bitfieldAdd( tr_bitfield_t*, size_t bit ); +void tr_bitfieldRem( tr_bitfield_t*, size_t bit ); +void tr_bitfieldAddRange( tr_bitfield_t *, size_t first, size_t last ); +void tr_bitfieldRemRange ( tr_bitfield_t*, size_t first, size_t last ); + +int tr_bitfieldIsEmpty( const tr_bitfield_t* ); +int tr_bitfieldHas( const tr_bitfield_t *, size_t bit ); +size_t tr_bitfieldCountTrueBits( const tr_bitfield_t* ); + +tr_bitfield_t* tr_bitfieldNegate( tr_bitfield_t* ); +tr_bitfield_t* tr_bitfieldAnd( tr_bitfield_t*, const tr_bitfield_t* ); + + + #endif diff --git a/macosx/CTGradient/CTGradientAdditions.h b/macosx/CTGradient/CTGradientAdditions.h index 9151549ca..722b836fd 100644 --- a/macosx/CTGradient/CTGradientAdditions.h +++ b/macosx/CTGradient/CTGradientAdditions.h @@ -32,6 +32,7 @@ @interface CTGradient (ProgressBar) + (CTGradient *)progressWhiteGradient; + (CTGradient *)progressGrayGradient; ++ (CTGradient *)progressLightGrayGradient; + (CTGradient *)progressBlueGradient; + (CTGradient *)progressGreenGradient; + (CTGradient *)progressLightGreenGradient; diff --git a/macosx/CTGradient/CTGradientAdditions.m b/macosx/CTGradient/CTGradientAdditions.m index 5d29b4726..5edd3d668 100644 --- a/macosx/CTGradient/CTGradientAdditions.m +++ b/macosx/CTGradient/CTGradientAdditions.m @@ -104,17 +104,17 @@ color1.position = 0; CTGradientElement color2; - color2.red = color2.green = color2.blue = 0.8; + color2.red = color2.green = color2.blue = 0.83; color2.alpha = 1.00; - color2.position = 0.5; + color2.position = 11.5/23; CTGradientElement color3; color3.red = color3.green = color3.blue = 0.95; color3.alpha = 1.00; - color3.position = 0.5; + color3.position = 11.5/23; CTGradientElement color4; - color4.red = color4.green = color4.blue = 0.95; + color4.red = color4.green = color4.blue = 0.92; color4.alpha = 1.00; color4.position = 1; @@ -158,6 +158,38 @@ return [newInstance autorelease]; } ++ (CTGradient *)progressLightGrayGradient +{ + CTGradient *newInstance = [[[self class] alloc] init]; + + CTGradientElement color1; + color1.red = color1.green = color1.blue = 0.87; + color1.alpha = 1.00; + color1.position = 0; + + CTGradientElement color2; + color2.red = color2.green = color2.blue = 0.754; + color2.alpha = 1.00; + color2.position = 0.5; + + CTGradientElement color3; + color3.red = color3.green = color3.blue = 0.87; + color3.alpha = 1.00; + color3.position = 0.5; + + CTGradientElement color4; + color4.red = color4.green = color4.blue = 0.87; + color4.alpha = 1.00; + color4.position = 1; + + [newInstance addElement:&color1]; + [newInstance addElement:&color2]; + [newInstance addElement:&color3]; + [newInstance addElement:&color4]; + + return [newInstance autorelease]; +} + + (CTGradient *)progressBlueGradient { CTGradient *newInstance = [[[self class] alloc] init]; diff --git a/macosx/Controller.h b/macosx/Controller.h index 73d375f60..f56b441f5 100644 --- a/macosx/Controller.h +++ b/macosx/Controller.h @@ -104,7 +104,9 @@ } - (void) openFiles: (NSArray *) filenames; -- (void) openFiles: (NSArray *) filenames ignoreDownloadFolder: (BOOL) ignore forceDeleteTorrent: (BOOL) delete; +- (void) openFiles: (NSArray *) filenames forcePath: (NSString *) path ignoreDownloadFolder: (BOOL) ignore + forceDeleteTorrent: (BOOL) delete; +- (void) openCreatedFile: (NSNotification *) notification; - (void) openFilesWithDict: (NSDictionary *) dictionary; - (void) openFilesAsk: (NSMutableArray *) files forceDeleteTorrent: (BOOL) delete; - (void) openFilesAskWithDict: (NSDictionary *) dictionary; @@ -116,6 +118,8 @@ - (void) quitSheetDidEnd: (NSWindow *) sheet returnCode: (int) returnCode contextInfo: (void *) contextInfo; +- (void) createFile: (id) sender; + - (void) resumeSelectedTorrents: (id) sender; - (void) resumeAllTorrents: (id) sender; - (void) resumeTorrents: (NSArray *) torrents; diff --git a/macosx/Controller.m b/macosx/Controller.m index 7393489bd..ce2847fd1 100644 --- a/macosx/Controller.m +++ b/macosx/Controller.m @@ -28,6 +28,7 @@ #import "Torrent.h" #import "TorrentCell.h" #import "TorrentTableView.h" +#import "CreatorWindowController.h" #import "StringAdditions.h" #import "UKKQueue.h" #import "ActionMenuSpeedToDisplayLimitTransformer.h" @@ -38,6 +39,7 @@ #import +#define TOOLBAR_CREATE @"Toolbar Create" #define TOOLBAR_OPEN @"Toolbar Open" #define TOOLBAR_REMOVE @"Toolbar Remove" #define TOOLBAR_INFO @"Toolbar Info" @@ -170,7 +172,6 @@ static void sleepCallBack(void * controller, io_service_t y, natural_t messageTy fPrefsController = [[PrefsController alloc] initWithWindowNibName: @"PrefsWindow" handle: fLib]; fBadger = [[Badger alloc] initWithLib: fLib]; - fOverlayWindow = [[DragOverlayWindow alloc] initWithLib: fLib]; fIPCController = [[IPCController alloc] init]; [fIPCController setDelegate: self]; @@ -195,7 +196,8 @@ static void sleepCallBack(void * controller, io_service_t y, natural_t messageTy [fTorrents release]; [fDisplayedTorrents release]; [fBadger release]; - [fOverlayWindow release]; + if (fOverlayWindow) + [fOverlayWindow release]; [fIPCController release]; [fAutoImportedNames release]; @@ -212,7 +214,6 @@ static void sleepCallBack(void * controller, io_service_t y, natural_t messageTy [fFilterBar setBackgroundImage: [NSImage imageNamed: @"FilterBarBackground.png"]]; [fWindow setAcceptsMouseMovedEvents: YES]; //ensure filter buttons display correctly - [fWindow addChildWindow: fOverlayWindow ordered: NSWindowAbove]; fToolbar = [[NSToolbar alloc] initWithIdentifier: @"Transmission Toolbar"]; [fToolbar setDelegate: self]; @@ -414,6 +415,10 @@ static void sleepCallBack(void * controller, io_service_t y, natural_t messageTy //change that just impacts the dock badge [nc addObserver: self selector: @selector(updateDockBadge:) name: @"DockBadgeChange" object: nil]; + + //open newly created torrent file + [nc addObserver: self selector: @selector(openCreatedFile:) + name: @"OpenCreatedTorrentFile" object: nil]; //timer to update the interface every second [self updateUI]; @@ -632,7 +637,7 @@ static void sleepCallBack(void * controller, io_service_t y, natural_t messageTy { NSString * path = [[fPendingTorrentDownloads objectForKey: [[download request] URL]] objectForKey: @"Path"]; - [self openFiles: [NSArray arrayWithObject: path] ignoreDownloadFolder: + [self openFiles: [NSArray arrayWithObject: path] forcePath: nil ignoreDownloadFolder: ![[fDefaults stringForKey: @"DownloadChoice"] isEqualToString: @"Constant"] forceDeleteTorrent: YES]; [fPendingTorrentDownloads removeObjectForKey: [[download request] URL]]; @@ -644,19 +649,20 @@ static void sleepCallBack(void * controller, io_service_t y, natural_t messageTy - (void) application: (NSApplication *) app openFiles: (NSArray *) filenames { - [self openFiles: filenames ignoreDownloadFolder: NO forceDeleteTorrent: NO]; + [self openFiles: filenames forcePath: nil ignoreDownloadFolder: NO forceDeleteTorrent: NO]; } -- (void) openFiles: (NSArray *) filenames ignoreDownloadFolder: (BOOL) ignore forceDeleteTorrent: (BOOL) delete +- (void) openFiles: (NSArray *) filenames forcePath: (NSString *) path ignoreDownloadFolder: (BOOL) ignore + forceDeleteTorrent: (BOOL) delete { NSString * downloadChoice = [fDefaults stringForKey: @"DownloadChoice"]; - if (ignore || [downloadChoice isEqualToString: @"Ask"]) + if (ignore || (!path && [downloadChoice isEqualToString: @"Ask"])) { [self openFilesAsk: [filenames mutableCopy] forceDeleteTorrent: delete]; return; } - if ([fDefaults boolForKey: @"UseIncompleteDownloadFolder"] + if (!path && [fDefaults boolForKey: @"UseIncompleteDownloadFolder"] && access([[[fDefaults stringForKey: @"IncompleteDownloadFolder"] stringByExpandingTildeInPath] UTF8String], 0)) { NSOpenPanel * panel = [NSOpenPanel openPanel]; @@ -680,7 +686,7 @@ static void sleepCallBack(void * controller, io_service_t y, natural_t messageTy didEndSelector: @selector(incompleteChoiceClosed:returnCode:contextInfo:) contextInfo: dict]; return; } - if ([downloadChoice isEqualToString: @"Constant"] + if (!path && [downloadChoice isEqualToString: @"Constant"] && access([[[fDefaults stringForKey: @"DownloadFolder"] stringByExpandingTildeInPath] UTF8String], 0)) { NSOpenPanel * panel = [NSOpenPanel openPanel]; @@ -711,13 +717,17 @@ static void sleepCallBack(void * controller, io_service_t y, natural_t messageTy { if (!(torrent = [[Torrent alloc] initWithPath: torrentPath forceDeleteTorrent: delete lib: fLib])) continue; - + //add it to the "File > Open Recent" menu [[NSDocumentController sharedDocumentController] noteNewRecentDocumentURL: [NSURL fileURLWithPath: torrentPath]]; - - NSString * folder = [downloadChoice isEqualToString: @"Constant"] - ? [[fDefaults stringForKey: @"DownloadFolder"] stringByExpandingTildeInPath] - : [torrentPath stringByDeletingLastPathComponent]; + + NSString * folder; + if (path) + folder = path; + else if ([downloadChoice isEqualToString: @"Constant"]) + folder = [[fDefaults stringForKey: @"DownloadFolder"] stringByExpandingTildeInPath]; + else + folder = [torrentPath stringByDeletingLastPathComponent]; [torrent setDownloadFolder: folder]; [torrent update]; @@ -729,6 +739,14 @@ static void sleepCallBack(void * controller, io_service_t y, natural_t messageTy [self updateTorrentsInQueue]; } +- (void) openCreatedFile: (NSNotification *) notification +{ + NSDictionary * dict = [notification userInfo]; + [self openFiles: [NSArray arrayWithObject: [dict objectForKey: @"File"]] forcePath: [dict objectForKey: @"Path"] + ignoreDownloadFolder: NO forceDeleteTorrent: NO]; + [dict release]; +} + - (void) incompleteChoiceClosed: (NSOpenPanel *) openPanel returnCode: (int) code contextInfo: (NSDictionary *) dictionary { if (code == NSOKButton) @@ -752,7 +770,7 @@ static void sleepCallBack(void * controller, io_service_t y, natural_t messageTy - (void) openFilesWithDict: (NSDictionary *) dictionary { - [self openFiles: [dictionary objectForKey: @"Filenames"] + [self openFiles: [dictionary objectForKey: @"Filenames"] forcePath: nil ignoreDownloadFolder: [[dictionary objectForKey: @"Ignore"] boolValue] forceDeleteTorrent: [[dictionary objectForKey: @"Delete"] boolValue]]; @@ -842,7 +860,7 @@ static void sleepCallBack(void * controller, io_service_t y, natural_t messageTy - (void) openFiles: (NSArray *) filenames { - [self openFiles: filenames ignoreDownloadFolder: NO forceDeleteTorrent: NO]; + [self openFiles: filenames forcePath: nil ignoreDownloadFolder: NO forceDeleteTorrent: NO]; } - (void) openShowSheet: (id) sender @@ -870,7 +888,7 @@ static void sleepCallBack(void * controller, io_service_t y, natural_t messageTy - (void) openFromSheet: (NSDictionary *) dictionary { - [self openFiles: [dictionary objectForKey: @"Files"] + [self openFiles: [dictionary objectForKey: @"Files"] forcePath: nil ignoreDownloadFolder: [[dictionary objectForKey: @"Ignore"] boolValue] forceDeleteTorrent: NO]; [dictionary release]; @@ -929,6 +947,11 @@ static void sleepCallBack(void * controller, io_service_t y, natural_t messageTy } } +- (void) createFile: (id) sender +{ + [CreatorWindowController createTorrentFile: fLib]; +} + - (void) resumeSelectedTorrents: (id) sender { [self resumeTorrents: [fDisplayedTorrents objectsAtIndexes: [fTableView selectedRowIndexes]]]; @@ -1635,7 +1658,7 @@ static void sleepCallBack(void * controller, io_service_t y, natural_t messageTy NSString * filterType = [fDefaults stringForKey: @"Filter"]; BOOL filtering = ![filterType isEqualToString: @"None"]; - int downloading = 0, seeding = 0, paused = 0, all = 0; + int downloading = 0, seeding = 0, paused = 0; BOOL isDownloading = [filterType isEqualToString: @"Download"], isSeeding = [filterType isEqualToString: @"Seed"], isPaused = [filterType isEqualToString: @"Pause"]; @@ -2121,11 +2144,12 @@ static void sleepCallBack(void * controller, io_service_t y, natural_t messageTy NSPasteboard * pasteboard = [info draggingPasteboard]; if ([[pasteboard types] containsObject: NSFilenamesPboardType]) { - //check if any files can be added + //check if any torrent files can be added NSArray * files = [pasteboard propertyListForType: NSFilenamesPboardType]; NSEnumerator * enumerator = [files objectEnumerator]; NSString * file; tr_torrent_t * tempTor; + BOOL torrent = NO; while ((file = [enumerator nextObject])) { int error; @@ -2133,14 +2157,33 @@ static void sleepCallBack(void * controller, io_service_t y, natural_t messageTy { tr_torrentClose(tempTor); - [fOverlayWindow setFiles: files]; + if (!fOverlayWindow) + fOverlayWindow = [[DragOverlayWindow alloc] initWithLib: fLib forWindow: fWindow]; + [fOverlayWindow setTorrents: files]; return NSDragOperationCopy; } + else + { + if (error == TR_EUNSUPPORTED || error == TR_EDUPLICATE) + torrent = YES; + } + } + + //create a torrent file if a single file + if (!torrent && [files count] == 1) + { + if (!fOverlayWindow) + fOverlayWindow = [[DragOverlayWindow alloc] initWithLib: fLib forWindow: fWindow]; + [fOverlayWindow setFile: [[files objectAtIndex: 0] lastPathComponent]]; + + return NSDragOperationCopy; } } else if ([[pasteboard types] containsObject: NSURLPboardType]) { + if (!fOverlayWindow) + fOverlayWindow = [[DragOverlayWindow alloc] initWithLib: fLib forWindow: fWindow]; [fOverlayWindow setURL: [[NSURL URLFromPasteboard: pasteboard] relativeString]]; return NSDragOperationCopy; @@ -2152,19 +2195,24 @@ static void sleepCallBack(void * controller, io_service_t y, natural_t messageTy - (void) draggingExited: (id ) info { - [fOverlayWindow fadeOut]; + if (fOverlayWindow) + [fOverlayWindow fadeOut]; } - (BOOL) performDragOperation: (id ) info { - [fOverlayWindow fadeOut]; + if (fOverlayWindow) + [fOverlayWindow fadeOut]; NSPasteboard * pasteboard = [info draggingPasteboard]; if ([[pasteboard types] containsObject: NSFilenamesPboardType]) { + BOOL torrent = NO, accept = YES; + //create an array of files that can be opened NSMutableArray * filesToOpen = [[NSMutableArray alloc] init]; - NSEnumerator * enumerator = [[pasteboard propertyListForType: NSFilenamesPboardType] objectEnumerator]; + NSArray * files = [pasteboard propertyListForType: NSFilenamesPboardType]; + NSEnumerator * enumerator = [files objectEnumerator]; NSString * file; tr_torrent_t * tempTor; while ((file = [enumerator nextObject])) @@ -2174,13 +2222,28 @@ static void sleepCallBack(void * controller, io_service_t y, natural_t messageTy { tr_torrentClose(tempTor); [filesToOpen addObject: file]; + + torrent = YES; + } + else + { + if (error == TR_EUNSUPPORTED || error == TR_EDUPLICATE) + torrent = YES; } } - [self application: NSApp openFiles: filesToOpen]; + if ([filesToOpen count] > 0) + [self application: NSApp openFiles: filesToOpen]; + else + { + if (!torrent && [files count] == 1) + [CreatorWindowController createTorrentFile: fLib forFile: [files objectAtIndex: 0]]; + else + accept = NO; + } [filesToOpen release]; - return YES; + return accept; } else if ([[pasteboard types] containsObject: NSURLPboardType]) { @@ -2397,7 +2460,17 @@ static void sleepCallBack(void * controller, io_service_t y, natural_t messageTy { NSToolbarItem * item = [[NSToolbarItem alloc] initWithItemIdentifier: ident]; - if ([ident isEqualToString: TOOLBAR_OPEN]) + if ([ident isEqualToString: TOOLBAR_CREATE]) + { + [item setLabel: NSLocalizedString(@"Create", "Create toolbar item -> label")]; + [item setPaletteLabel: NSLocalizedString(@"Create Torrent File", "Create toolbar item -> palette label")]; + [item setToolTip: NSLocalizedString(@"Create torrent file", "Create toolbar item -> tooltip")]; + [item setImage: [NSImage imageNamed: @"Create.png"]]; + [item setTarget: self]; + [item setAction: @selector(createFile:)]; + [item setAutovalidates: NO]; + } + else if ([ident isEqualToString: TOOLBAR_OPEN]) { [item setLabel: NSLocalizedString(@"Open", "Open toolbar item -> label")]; [item setPaletteLabel: NSLocalizedString(@"Open Torrent Files", "Open toolbar item -> palette label")]; @@ -2484,7 +2557,7 @@ static void sleepCallBack(void * controller, io_service_t y, natural_t messageTy - (NSArray *) toolbarAllowedItemIdentifiers: (NSToolbar *) t { return [NSArray arrayWithObjects: - TOOLBAR_OPEN, TOOLBAR_REMOVE, + TOOLBAR_CREATE, TOOLBAR_OPEN, TOOLBAR_REMOVE, TOOLBAR_PAUSE_SELECTED, TOOLBAR_RESUME_SELECTED, TOOLBAR_PAUSE_ALL, TOOLBAR_RESUME_ALL, TOOLBAR_FILTER, TOOLBAR_INFO, NSToolbarSeparatorItemIdentifier, @@ -2496,7 +2569,7 @@ static void sleepCallBack(void * controller, io_service_t y, natural_t messageTy - (NSArray *) toolbarDefaultItemIdentifiers: (NSToolbar *) t { return [NSArray arrayWithObjects: - TOOLBAR_OPEN, TOOLBAR_REMOVE, + TOOLBAR_CREATE, TOOLBAR_OPEN, TOOLBAR_REMOVE, NSToolbarSeparatorItemIdentifier, TOOLBAR_PAUSE_ALL, TOOLBAR_RESUME_ALL, NSToolbarFlexibleSpaceItemIdentifier, @@ -2850,13 +2923,13 @@ static void sleepCallBack(void * controller, io_service_t y, natural_t messageTy NSEnumerator * enumerator = [fTorrents objectEnumerator]; Torrent * torrent; while ((torrent = [enumerator nextObject])) - if ([torrent isActive]) - { - if ([torrent allDownloaded]) - seeding++; - else - downloading++; - } + { + if ([torrent isSeeding]) + seeding++; + else if ([torrent isActive]) + downloading++; + else; + } NSMenuItem * seedingItem = [fDockMenu itemWithTag: DOCK_SEEDING_TAG], * downloadingItem = [fDockMenu itemWithTag: DOCK_DOWNLOADING_TAG]; diff --git a/macosx/CreatorWindowController.h b/macosx/CreatorWindowController.h new file mode 100644 index 000000000..7a5a38804 --- /dev/null +++ b/macosx/CreatorWindowController.h @@ -0,0 +1,57 @@ +/****************************************************************************** + * $Id$ + * + * Copyright (c) 2007 Transmission authors and contributors + * + * 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, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * 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. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + *****************************************************************************/ + +#import +#import "transmission.h" +#import "makemeta.h" + +@interface CreatorWindowController : NSWindowController +{ + IBOutlet NSImageView * fIcon; + IBOutlet NSTextField * fNameField, * fStatusField, * fPiecesField, * fTrackerField, * fLocationField; + IBOutlet NSTextView * fCommentView; + IBOutlet NSButton * fPrivateCheck, * fOpenCheck; + + IBOutlet NSView * fProgressView; + IBOutlet NSProgressIndicator * fProgressIndicator; + + tr_metainfo_builder_t * fInfo; + NSString * fPath, * fLocation; + BOOL fOpenTorrent; + + NSTimer * fTimer; + BOOL fStarted; +} + ++ (void) createTorrentFile: (tr_handle_t *) handle; ++ (void) createTorrentFile: (tr_handle_t *) handle forFile: (NSString *) file; + +- (id) initWithWindowNibName: (NSString *) name handle: (tr_handle_t *) handle path: (NSString *) path; + +- (void) setLocation: (id) sender; +- (void) create: (id) sender; +- (void) cancelCreateWindow: (id) sender; +- (void) cancelCreateProgress: (id) sender; + +@end diff --git a/macosx/CreatorWindowController.m b/macosx/CreatorWindowController.m new file mode 100644 index 000000000..8ba1d1737 --- /dev/null +++ b/macosx/CreatorWindowController.m @@ -0,0 +1,340 @@ +/****************************************************************************** + * $Id$ + * + * Copyright (c) 2007 Transmission authors and contributors + * + * 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, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * 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. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + *****************************************************************************/ + +#import "CreatorWindowController.h" +#import "StringAdditions.h" + +#define DEFAULT_SAVE_LOCATION @"~/Desktop/" + +@interface CreatorWindowController (Private) + ++ (NSString *) chooseFile; +- (void) locationSheetClosed: (NSSavePanel *) openPanel returnCode: (int) code contextInfo: (void *) info; +- (void) checkProgress; +- (void) failureSheetClosed: (NSAlert *) alert returnCode: (int) code contextInfo: (void *) info; + +@end + +@implementation CreatorWindowController + ++ (void) createTorrentFile: (tr_handle_t *) handle +{ + //get file/folder for torrent + NSString * path; + if (!(path = [CreatorWindowController chooseFile])) + return; + + CreatorWindowController * creator = [[self alloc] initWithWindowNibName: @"Creator" handle: handle path: path]; + [creator showWindow: nil]; +} + ++ (void) createTorrentFile: (tr_handle_t *) handle forFile: (NSString *) file +{ + CreatorWindowController * creator = [[self alloc] initWithWindowNibName: @"Creator" handle: handle path: file]; + [creator showWindow: nil]; +} + +- (id) initWithWindowNibName: (NSString *) name handle: (tr_handle_t *) handle path: (NSString *) path +{ + if ((self = [super initWithWindowNibName: name])) + { + fStarted = NO; + + fPath = [path retain]; + fInfo = tr_metaInfoBuilderCreate(handle, [fPath UTF8String]); + if (fInfo->fileCount == 0) + { + NSAlert * alert = [[[NSAlert alloc] init] autorelease]; + [alert addButtonWithTitle: NSLocalizedString(@"OK", "Create torrent -> no files -> button")]; + [alert setMessageText: NSLocalizedString(@"This folder contains no files.", + "Create torrent -> no files -> title")]; + [alert setInformativeText: NSLocalizedString(@"There must be at least one file in a folder to create a torrent file.", + "Create torrent -> no files -> warning")]; + [alert setAlertStyle: NSWarningAlertStyle]; + + [alert runModal]; + + [self release]; + return nil; + } + } + return self; +} + +- (void) awakeFromNib +{ + NSString * name = [fPath lastPathComponent]; + + [[self window] setTitle: name]; + + [fNameField setStringValue: name]; + [fNameField setToolTip: fPath]; + + BOOL multifile = !fInfo->isSingleFile; + + #warning fix when resizing window + [fIcon setImage: [[NSWorkspace sharedWorkspace] iconForFileType: multifile + ? NSFileTypeForHFSTypeCode('fldr') : [fPath pathExtension]]]; + + NSString * statusString = [NSString stringForFileSize: fInfo->totalSize]; + if (multifile) + { + NSString * fileString; + int count = fInfo->fileCount; + if (count != 1) + fileString = [NSString stringWithFormat: NSLocalizedString(@"%d Files, ", "Create torrent -> info"), count]; + else + fileString = NSLocalizedString(@"1 File, ", "Create torrent -> info"); + statusString = [fileString stringByAppendingString: statusString]; + } + [fStatusField setStringValue: statusString]; + + NSString * piecesCountString; + int piecesCount = fInfo->pieceCount; + if (piecesCount == 1) + piecesCountString = NSLocalizedString(@"1 piece", "Create torrent -> info"); + else + piecesCountString = [NSString stringWithFormat: NSLocalizedString(@"%d pieces", "Create torrent -> info"), + piecesCount]; + [fPiecesField setStringValue: [NSString stringWithFormat: NSLocalizedString(@"%@, %@ each", "Create torrent -> info"), + piecesCountString, [NSString stringForFileSize: fInfo->pieceSize]]]; + + fLocation = [[[DEFAULT_SAVE_LOCATION stringByAppendingPathComponent: [name stringByAppendingPathExtension: @"torrent"]] + stringByExpandingTildeInPath] retain]; + [fLocationField setStringValue: [fLocation stringByAbbreviatingWithTildeInPath]]; + [fLocationField setToolTip: fLocation]; +} + +- (void) dealloc +{ + [fPath release]; + if (fLocation) + [fLocation release]; + + if (fInfo) + tr_metaInfoBuilderFree(fInfo); + + if (fTimer) + [fTimer invalidate]; + + [super dealloc]; +} + +- (void) setLocation: (id) sender +{ + NSSavePanel * panel = [NSSavePanel savePanel]; + + [panel setPrompt: @"Select"]; + [panel setRequiredFileType: @"torrent"]; + [panel setCanSelectHiddenExtension: YES]; + + [panel beginSheetForDirectory: nil file: [fLocation lastPathComponent] modalForWindow: [self window] modalDelegate: self + didEndSelector: @selector(locationSheetClosed:returnCode:contextInfo:) contextInfo: nil]; +} + +- (void) create: (id) sender +{ + //parse tracker string + NSString * trackerString = [fTrackerField stringValue]; + if ([trackerString rangeOfString: @"://"].location != NSNotFound) + { + if (![trackerString hasPrefix: @"http://"]) + { + NSAlert * alert = [[[NSAlert alloc] init] autorelease]; + [alert addButtonWithTitle: NSLocalizedString(@"OK", "Create torrent -> http warning -> button")]; + [alert setMessageText: NSLocalizedString(@"The tracker address must begin with \"http://\".", + "Create torrent -> http warning -> title")]; + [alert setInformativeText: NSLocalizedString(@"Change the tracker address to create the torrent.", + "Create torrent -> http warning -> warning")]; + [alert setAlertStyle: NSWarningAlertStyle]; + + [alert beginSheetModalForWindow: [self window] modalDelegate: self didEndSelector: nil contextInfo: nil]; + return; + } + } + else + trackerString = [@"http://" stringByAppendingString: trackerString]; + + //don't allow blank addresses + if ([trackerString length] <= 7) + { + NSAlert * alert = [[[NSAlert alloc] init] autorelease]; + [alert addButtonWithTitle: NSLocalizedString(@"OK", "Create torrent -> no url warning -> button")]; + [alert setMessageText: NSLocalizedString(@"The tracker address cannot be blank.", + "Create torrent -> no url warning -> title")]; + [alert setInformativeText: NSLocalizedString(@"Change the tracker address to create the torrent.", + "Create torrent -> no url warning -> warning")]; + [alert setAlertStyle: NSWarningAlertStyle]; + + [alert beginSheetModalForWindow: [self window] modalDelegate: self didEndSelector: nil contextInfo: nil]; + return; + } + + //check if a file with the same name and location already exists + if ([[NSFileManager defaultManager] fileExistsAtPath: fLocation]) + { + NSArray * pathComponents = [fLocation pathComponents]; + int count = [pathComponents count]; + + NSAlert * alert = [[[NSAlert alloc] init] autorelease]; + [alert addButtonWithTitle: NSLocalizedString(@"OK", "Create torrent -> file already exists warning -> button")]; + [alert setMessageText: NSLocalizedString(@"A torrent file with this name and directory cannot be created.", + "Create torrent -> file already exists warning -> title")]; + [alert setInformativeText: [NSString stringWithFormat: + NSLocalizedString(@"A file with the name \"%@\" already exists in the directory \"%@\". " + "Choose a new name or directory to create the torrent.", + "Create torrent -> file already exists warning -> warning"), + [pathComponents objectAtIndex: count-1], [pathComponents objectAtIndex: count-2]]]; + [alert setAlertStyle: NSWarningAlertStyle]; + + [alert beginSheetModalForWindow: [self window] modalDelegate: self didEndSelector: nil contextInfo: nil]; + return; + } + + fOpenTorrent = [fOpenCheck state] == NSOnState; + tr_makeMetaInfo(fInfo, [fLocation UTF8String], [trackerString UTF8String], [[fCommentView string] UTF8String], + [fPrivateCheck state] == NSOnState); + + fTimer = [NSTimer scheduledTimerWithTimeInterval: 0.1 target: self selector: @selector(checkProgress) + userInfo: nil repeats: YES]; +} + +- (void) cancelCreateWindow: (id) sender +{ + [[self window] close]; +} + +- (void) windowWillClose: (NSNotification *) notification +{ + [self release]; +} + +- (void) cancelCreateProgress: (id) sender +{ + fInfo->abortFlag = 1; + [fTimer fire]; +} + +@end + +@implementation CreatorWindowController (Private) + ++ (NSString *) chooseFile +{ + NSOpenPanel * panel = [NSOpenPanel openPanel]; + + [panel setPrompt: NSLocalizedString(@"Select", "Create torrent -> select file")]; + [panel setAllowsMultipleSelection: NO]; + [panel setCanChooseFiles: YES]; + [panel setCanChooseDirectories: YES]; + [panel setCanCreateDirectories: NO]; + + [panel setMessage: NSLocalizedString(@"Select a file or folder for the torrent file.", "Create torrent -> select file")]; + + BOOL success = [panel runModal] == NSOKButton; + return success ? [[panel filenames] objectAtIndex: 0] : nil; +} + +- (void) locationSheetClosed: (NSSavePanel *) panel returnCode: (int) code contextInfo: (void *) info +{ + if (code == NSOKButton) + { + [fLocation release]; + fLocation = [[panel filename] retain]; + + [fLocationField setStringValue: [fLocation stringByAbbreviatingWithTildeInPath]]; + [fLocationField setToolTip: fLocation]; + } +} + +- (void) checkProgress +{ + if (fInfo->isDone) + { + [fTimer invalidate]; + fTimer = nil; + + if (fInfo->failed) + { + if (!fInfo->abortFlag) + { + NSAlert * alert = [[[NSAlert alloc] init] autorelease]; + [alert addButtonWithTitle: NSLocalizedString(@"OK", "Create torrent -> failed -> button")]; + [alert setMessageText: [NSString stringWithFormat: NSLocalizedString(@"Creation of \"%@\" failed.", + "Create torrent -> failed -> title"), [fLocation lastPathComponent]]]; + [alert setInformativeText: NSLocalizedString(@"There was an error parsing the data file. " + "The torrent file was not created.", "Create torrent -> failed -> warning")]; + [alert setAlertStyle: NSWarningAlertStyle]; + + [alert beginSheetModalForWindow: [self window] modalDelegate: self + didEndSelector: @selector(failureSheetClosed:returnCode:contextInfo:) contextInfo: nil]; + return; + } + } + else + { + if (fOpenTorrent) + { + NSDictionary * dict = [[NSDictionary alloc] initWithObjectsAndKeys: fLocation, @"File", + [fPath stringByDeletingLastPathComponent], @"Path", nil]; + [[NSNotificationCenter defaultCenter] postNotificationName: @"OpenCreatedTorrentFile" object: self userInfo: dict]; + } + } + + [[self window] close]; + } + else + { + [fProgressIndicator setDoubleValue: (double)fInfo->pieceIndex / fInfo->pieceCount]; + + if (!fStarted) + { + NSWindow * window = [self window]; + + NSRect windowRect = [window frame]; + float difference = [fProgressView frame].size.height - [[window contentView] frame].size.height; + windowRect.origin.y -= difference; + windowRect.size.height += difference; + + //don't allow vertical resizing + float height = windowRect.size.height; + [window setMinSize: NSMakeSize([window minSize].width, height)]; + [window setMaxSize: NSMakeSize([window maxSize].width, height)]; + + [window setContentView: fProgressView]; + [window setFrame: windowRect display: YES animate: YES]; + [fProgressView setHidden: NO]; + + fStarted = YES; + } + } +} + +- (void) failureSheetClosed: (NSAlert *) alert returnCode: (int) code contextInfo: (void *) info +{ + [[alert window] orderOut: nil]; + [[self window] close]; +} + +@end diff --git a/macosx/Credits.rtf b/macosx/Credits.rtf index 175d3b74f..44dabd275 100644 --- a/macosx/Credits.rtf +++ b/macosx/Credits.rtf @@ -1,7 +1,6 @@ {\rtf1\mac\ansicpg10000\cocoartf824\cocoasubrtf410 {\fonttbl\f0\fswiss\fcharset77 Helvetica;} {\colortbl;\red255\green255\blue255;} -\vieww9000\viewh8400\viewkind0 \pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\ql\qnatural\pardirnatural \f0\fs24 \cf0 Transmission is written and maintained by:\ @@ -21,6 +20,10 @@ Mitchell Livingston \ \ Bryan Varner \ + BeOS interface\ + \ +Charles Kerr \ + + Back-end\ + + GTK+ interface\ \ \ Translators:\ diff --git a/macosx/DragOverlayWindow.h b/macosx/DragOverlayWindow.h index d3b40e74f..00791ef6f 100644 --- a/macosx/DragOverlayWindow.h +++ b/macosx/DragOverlayWindow.h @@ -29,11 +29,13 @@ { tr_handle_t * fLib; - //NSTimer * fFadeInTimer, * fFadeOutTimer; NSViewAnimation * fFadeInAnimation, * fFadeOutAnimation; } -- (void) setFiles: (NSArray *) files; +- (id) initWithLib: (tr_handle_t *) lib forWindow: (NSWindow *) window; + +- (void) setTorrents: (NSArray *) files; +- (void) setFile: (NSString *) file; - (void) setURL: (NSString *) url; - (void) fadeOut; diff --git a/macosx/DragOverlayWindow.m b/macosx/DragOverlayWindow.m index e8ecb768d..b554f8de0 100644 --- a/macosx/DragOverlayWindow.m +++ b/macosx/DragOverlayWindow.m @@ -28,7 +28,7 @@ @implementation DragOverlayWindow -- (id) initWithLib: (tr_handle_t *) lib +- (id) initWithLib: (tr_handle_t *) lib forWindow: (NSWindow *) window { if (self = ([super initWithContentRect: NSMakeRect(0, 0, 1.0, 1.0) styleMask: NSBorderlessWindowMask backing: NSBackingStoreBuffered defer: NO])) @@ -57,6 +57,8 @@ NSViewAnimationFadeOutEffect, NSViewAnimationEffectKey, nil]]]; [fFadeOutAnimation setDuration: 0.5]; [fFadeOutAnimation setAnimationBlockingMode: NSAnimationNonblockingThreaded]; + + [window addChildWindow: self ordered: NSWindowAbove]; } return self; } @@ -69,7 +71,7 @@ [super dealloc]; } -- (void) setFiles: (NSArray *) files +- (void) setTorrents: (NSArray *) files { uint64_t size = 0; int count = 0; @@ -114,9 +116,9 @@ { NSString * fileString; if (fileCount == 1) - fileString = NSLocalizedString(@"1 File, ", "Drag overlay -> drag files"); + fileString = NSLocalizedString(@"1 File, ", "Drag overlay -> torrents"); else - fileString= [NSString stringWithFormat: NSLocalizedString(@"%d Files, ", "Drag overlay -> drag files"), fileCount]; + fileString= [NSString stringWithFormat: NSLocalizedString(@"%d Files, ", "Drag overlay -> torrents"), fileCount]; secondString = [fileString stringByAppendingString: secondString]; } @@ -124,7 +126,7 @@ icon = [[NSWorkspace sharedWorkspace] iconForFileType: folder ? NSFileTypeForHFSTypeCode('fldr') : [name pathExtension]]; else { - name = [NSString stringWithFormat: NSLocalizedString(@"%d Torrent Files", "Drag overlay -> drag files"), count]; + name = [NSString stringWithFormat: NSLocalizedString(@"%d Torrent Files", "Drag overlay -> torrents"), count]; secondString = [secondString stringByAppendingString: @" Total"]; } @@ -140,6 +142,23 @@ [fFadeInAnimation startAnimation]; } +- (void) setFile: (NSString *) file +{ + + [[self contentView] setOverlay: [NSImage imageNamed: @"Create.png"] + mainLine: NSLocalizedString(@"Create a Torrent File", "Drag overlay -> file") subLine: file]; + + //stop other animation and set to same progress + if ([fFadeOutAnimation isAnimating]) + { + [fFadeOutAnimation stopAnimation]; + [fFadeInAnimation setCurrentProgress: 1.0 - [fFadeOutAnimation currentProgress]]; + } + [self setFrame: [[self parentWindow] frame] display: YES]; + [fFadeInAnimation startAnimation]; +} + + - (void) setURL: (NSString *) url { [[self contentView] setOverlay: [NSImage imageNamed: @"Globe.tiff"] diff --git a/macosx/English.lproj/Creator.nib/classes.nib b/macosx/English.lproj/Creator.nib/classes.nib new file mode 100644 index 000000000..486f0aa2d --- /dev/null +++ b/macosx/English.lproj/Creator.nib/classes.nib @@ -0,0 +1,30 @@ +{ + IBClasses = ( + { + ACTIONS = { + cancelCreateProgress = id; + cancelCreateWindow = id; + create = id; + setLocation = id; + }; + CLASS = CreatorWindowController; + LANGUAGE = ObjC; + OUTLETS = { + fCommentView = NSTextView; + fIcon = NSImageView; + fLocationField = NSTextField; + fNameField = NSTextField; + fOpenCheck = NSButton; + fPiecesField = NSTextField; + fPrivateCheck = NSButton; + fProgressIndicator = NSProgressIndicator; + fProgressView = NSView; + fStatusField = NSTextField; + fTrackerField = NSTextField; + }; + SUPERCLASS = NSWindowController; + }, + {CLASS = FirstResponder; LANGUAGE = ObjC; SUPERCLASS = NSObject; } + ); + IBVersion = 1; +} \ No newline at end of file diff --git a/macosx/English.lproj/Creator.nib/info.nib b/macosx/English.lproj/Creator.nib/info.nib new file mode 100644 index 000000000..fd4be32c3 --- /dev/null +++ b/macosx/English.lproj/Creator.nib/info.nib @@ -0,0 +1,22 @@ + + + + + IBDocumentLocation + 52 61 356 240 0 0 1152 842 + IBEditorPositions + + 56 + 392 515 368 126 0 0 1152 842 + + IBFramework Version + 446.1 + IBOpenObjects + + 5 + 56 + + IBSystem Version + 8P135 + + diff --git a/macosx/English.lproj/Creator.nib/keyedobjects.nib b/macosx/English.lproj/Creator.nib/keyedobjects.nib new file mode 100644 index 0000000000000000000000000000000000000000..1ff0bafbe7c8cbdc5f8f74403a96fda8ad491a9e GIT binary patch literal 13046 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 literal 0 HcmV?d00001 diff --git a/macosx/English.lproj/InfoWindow.nib/classes.nib b/macosx/English.lproj/InfoWindow.nib/classes.nib index f4a6d38a9..7098a0b26 100644 --- a/macosx/English.lproj/InfoWindow.nib/classes.nib +++ b/macosx/English.lproj/InfoWindow.nib/classes.nib @@ -7,8 +7,10 @@ revealDataFile = id; revealFile = id; revealTorrentFile = id; + setCheck = id; setLimitSetting = id; setPex = id; + setPriority = id; setRatioLimit = id; setRatioSetting = id; setSpeedLimit = id; @@ -32,8 +34,13 @@ fDownloadedValidField = NSTextField; fDownloadingFromField = NSTextField; fErrorMessageView = NSTextView; + fFileCheckItem = NSMenuItem; fFileOutline = NSOutlineView; + fFilePriorityHigh = NSMenuItem; + fFilePriorityLow = NSMenuItem; + fFilePriorityNormal = NSMenuItem; fFileTableStatusField = NSTextField; + fFileUncheckItem = NSMenuItem; fHashField = NSTextField; fImageView = NSImageView; fLeechersField = NSTextField; diff --git a/macosx/English.lproj/InfoWindow.nib/info.nib b/macosx/English.lproj/InfoWindow.nib/info.nib index 6bbe05e14..669080d22 100644 --- a/macosx/English.lproj/InfoWindow.nib/info.nib +++ b/macosx/English.lproj/InfoWindow.nib/info.nib @@ -7,7 +7,7 @@ IBEditorPositions 549 - 364 417 144 49 0 0 1152 842 + 565 283 144 118 0 0 1152 842 IBFramework Version 446.1 diff --git a/macosx/English.lproj/InfoWindow.nib/keyedobjects.nib b/macosx/English.lproj/InfoWindow.nib/keyedobjects.nib index 6b1b29fc28d93ca24d6212b9550d2ddebd42fb63..af0336f3987850fd85405ed7314840ea10152e20 100644 GIT binary patch literal 48817 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%0DcS4r1Nw@7zM zlca~F@8lU|G}(cSB|DQ{$au0l*@qlV4kfe5JTjkLN-if?l7(auxrscBtR(l72g$=^ zJ^48KB>6A$S@PfHOXREM8|2&Md*lb?$K+?^m*juQ@5mp?|B}Cwe^91Skd&#E85C;@ zibAG@P{JsY6dEOp!l1-Z3Micv8KsA!pa6=J(oY$rtfOq8Y@+-|*+!Y5?4TT`+@yS? z{Gj{_L4;U_ObwYHViiI$^Yr27{Bv3eBP1q-6;c$^9?}^i3+WA!hxCOgLso{Y4p|$r zK4eqKmXK{B`$8^<{1@^qWC~bXw?)(7@23P*P}!+1E58lp0D8%?WJ^Z4PY> z?Fj7*m4)_(E)HE9x;%7c=$g>=p&LVg4?P|FHuQby$I#EAUqZiy{tTNE<{9Q4<{L%` zBZdWqk<6YyDPf{8ahN2mA*?Y>8rB+S3>y!d7q%d5aoDo36=9pgj)%PndlmLi*xRu8 zVV}ajgnbQn3CD-KhkJ$lhWmvR!inMRa6x!wxG=mXTpTV5ZwS|h&k5Iu8^h;@F9=^0 zzA}7&_+jSP>ZM$#i0kqMDW zk*Sf~$c)JB$g0TtNLgfWWM8B*QXM%IIXhAlxhC>x?{XM?R$5QPETfDwc|) z;;9}~Z>k@aKnLuz`>J92`>OJZt^&#~M^*arwIntbIt~57V7%h@UqtR(hS}HA_mO;y=71K&+)ig1! zj@Cfa(sVQf%|x3=TR>YxTSD7N+e-VLww<=}vSU9Z&b5d(i{v9C|W6mCmJS(6i{d^m@96K1SEk4fJvPeELHAa{3YaG5QJm zDf$`uS^9bUMfwZIG{y{u6$8bvWxxy!BZ5I?L@^kQ7)Bg}%}8ZbF@_i;j8VoIL&q>O z<}&6pmN2$3wlXFdI~cne#~EiB=NT6mml?MhcNvq6M~o-Y)1qfYTScRy;b{A4Of)u{ z9GxDW5uFvC8=V(j5Y3D3j_!?CM9+#=MGr&|MH`|wMPG=%6n!Q7di2fcJJI)}A4GqM z{uupl^ncOcnKPJnOf=Jh>BMwrdNF;O1ZE(!oLR{fGDXZ!0cxZ zFy}HCFc&kIF;_76GOsajFmE&OGAEf2nQxizV{BvK7<7zd3^v9&#x;f+6B`p3!;ay^ zq{O7fU06V-sVOV^d?fv01S>u~o6c*qYed*!oym?5tQ-tU7imb}UvOYl@v0yC8NV zc1P^)*uAj_Vh_b0iTyM7PVD{I2eFT1pT)k2{lKzj*|O|e4lE~@3(Jk=$?{?OvjSPc zED9@>Rm19L4YEd98rB?^fi=#W&sxM<%38r%&05Ra%R0(B$GX6}%(}+9$-2Y37dJi5 zDh?F~$6?~Iakw~qoO>K0ju=OZqr|D=7RPOfI}vv!ZZdvKyh}VYJ~h4|zB;}+z9U{1 z-y7c--yc65KN3G0KNfF{pBuj*esTP=_?7W%;@8D*i@zFwBmQ>$z4!<5kK&)lzhci| zJFuPDE^IfpC)YOmBswNKC%Po!6Um9RM0R3fVq0Q+VrSw|qBe1U z;_Ae8i5nAtOWcyUJMnPhiNqU;4-%gye&Tp>LO3iAo5SIxaWXiWoE%Oerz#|r6$FgGB;&G%F>ioDQi>KrEE;u znlh2HGi86ujg;Fd_fjTP9;G}@d67CTb!Mt{Dk>FDMW_0t`lkk_2B(rzqf$kwwW;-~ z&8Z!!vefQWd8#s1ojQ~{Cv|h`*3^mA9jUuh_oe=kdMovA>iyIQsgF~irhZBNmijZz z?6{ImUc4jRNA?;3u!OX zr>5Jc+owCEW7C7vIq5a&lJtgjX?kmVTY6`@EWJ0~kiI>ASNh)c{pknO52YVTKc4<1 z{aN~p^jGO`(%+|lO#i{faHF^kE|VL}jpHV8lej6|3~m#*h1<^U;`VTRxqV!~?dLAy z9^fA09_60kp5p$+J;OcEean5%{mA{y{g3;d`!mBXBP1g{gPK9lU}nT*urk;g{EXg= zz6@oCI%6nfIAeB(He*Z1wv3649T~eb_GKK%IFs=xb4I3BrcI`8CY*`M#AZ5Yx@AUY z7G@S_mSzevt1^X|qRiS%ZRVUzeWo#UJac~LqRb_kJ2J0k-pIV2c`x%p=EKa#na{Fj zWLag|WZ7ofXE|hHv)r;GvWl}xvdXdqS(RDUS>h~7*6b{8mM+VfH8*Qs)`G0XSv#{X zWnIm>k##%kPS(Aw2ieGM%j{{{GqSC-ZL{HQ-)v@fWwtO|lr7GdWH)3>vs<#qvh~@f z?0MPqvlnGA$zGnlC;Mjh?d-eR_p={nKgoWU{W=Gc2G3Rp5)tu`&w{q_0+|PNO z>zM1D>zeDH>y_)9OUNbWl5#1z?A-L++}wg(UT#TlS?*x&NUkP#POc$$Ja>NXqTD69 zYjZc`ZqD73H#KiYo^>9a=a`4h!{y=gJo3Er{PF_wg7V0DA$j3>oV>ie^1PwE*?HPL zU7j&-Zr-xI6?tp(HstNgJCJuM?`Yo5yoY%&@?Ph?&HIq|Dc>#MGv6maAfJ>Uk{_BM zkx$EK<`?Ie<_q#G^Q-g4`R)0g`NsUY`SbG^<}b-#mVY?^SpLcUGx_K9FXms#zm@+b z|6Bggf++>af~f_J0%ifLfL)MSkX*nm$SlY%C@82Ys4r+NkQTHQ^b`yhj1*`J#tW7f ztSZ=BaI4^M!DPXcg69RV3f>faDRd}wDs(Bt7kU(W7qSX73o8nRg*Ane!iK_@LRH~V z;Yi_V;aK6k!li{P3Rf4dE8I}HtMGi`qr%69&kA1_zA5}t_@l_G2vdYD!WH=!(TkWx ztfG{noFZY7v}mwsYtcl}&Z0d<`-=`19WFXibgF2w=m*b{N9Kj_!g-OrC|)!#hR5OY zd1X8SuZkz)iFwUDJEesUH;P{s|HrrH zqxtUqKz<}Ym0!Xy=L`5%d=bBvU(avkH}RYKZTt?tjNi-e<16`U{s7;=U%_9^U&r6b z-^}02pWyH0ALswcKg~bKzrerDzsA4Gzs-Ni|59RK5?B&d!Y#=u$t@9-G?Yk7T1z@g zR3-f->XN~dIVGl&xh2a=R+g+OnJC#=vZrK!$)S=vC9g`pltQJbQe3HTDXlcBG`f^o z$|+4N6_-j%TT8o3wWTXdSC_6U-B7xzbZ6xpg_J+^*cd+@aj599K>*&nnL?FDU1gmz0;5mzP(T_mnRxUs}GRe0BM{ z^7Z8#%YQ4sRDPxWdikyLJLUJwC(9oS0tLYWk|0D7E}#nNf@lF#5G#lmBnXlOsRFJb zOOPwb6I2MKf>uF?KqlxBC1RDgK1bYPg1qTI31g8b( z1Q!H%1(SkDf~OTzDm*KED*P(~D}pOX6_kpwioA+~ilU0*iqZ-}MP)@z#ZblU3T=h1 z!dPLdm{+l|Vn^ka%4wDMl|GfBl}VMn%96_JN=aoyrL?l8vc0mqvZqo}2`W{U{gtN5 zxs~%PS5&U5yij?s@Uz~=)sw0>RbQ*V3oV7#LR+D|&_Re5vV?3Q zN0=f^6J`jrg?Yk4VTEu+s1eQ)8iXd{JmEs&65(>;TH#jV4&fidv%*Q?Tj77zQ>!u6 zLDk&q%SpT0K^+uQpYWSI?_nSiQaaO7->XTh(`~C#xS+KdOEz zvKFC4um~e^6gi7kFW6j)}1vT4ieu)ucOYwB^OtFm^C5FWq zv7;C(_7HoCGsQXLd~uPuSX?R=h^xdRajUpPEED&N)#5?%9PwQ70`X$;GVu!WY4JJn z1@UF^Rq+k+ZSg(vgIcRvR4rVKsdcP%u63<-ul1^>*YayiYs+gZYK7*k8GfS2AC+Lb6`6NwP)qyJWlMj^w`Nq2!6=ndGJ9AIUq($2yC;z`Ec% zN?ll8L>;Y;Q5RDeSC?5=Q#VvMQa4(st<%*R>&EL=)g7)oR(G=Que#H9XY2m1dtN`Y z-nt%DZcdU1+_pFbpkE>6pPpVI@Pp#+Hi|QBFFRou!zoLG1{kr-M^_%K{tKV9` zqyA$3t@=mx-x|;j_y&3dyCJ!O-%#1m+R)z6)zID0+c4NLuVF#M;)bOS%NsT|9B4S$ zaHQc_!-1)&Xre9J!DO&0%b&}$wp;ES#BTbQ}Nx4$Kv|idIZIQM~JEUsqxOBdB zk#vc4nRK`Gl=O`By!3+flJu$ci}aiHXY-V1WV3ZMsu^x}Z4PdxG>0`uG*g?|&3Vm* z&Be_n&E?I~=9cF6=C0=M=F#TG%}bkCG_P)6+q}Q|Z1ef%i_MptuQtDE{?`1XWl9U8 z1!{3=32Gs=gtUaUM6|@W2U`xe9Bnz* za<65w*|x8ywYF8>s%_P^ z8e7L(=e2HXJ)ST9{noavZF}4Pw#RL6 z+TOQ)YWvdmt?ftqly+qM)OLqy+BfcZ4qo{-5QPv^osO;$I80;A7&~%J-=sMPR?C#jt@khs@jw2n{JDzvE>Uh)f zuH!@Jj80r9zSE=AtJ9~G(izvu?&Nf)bf$NfcS<{3I@>$DI(s@tJGGsO9(cqVrnk&CWZW_c|v#zjV#$vhK3&g1gXN_%2cxr7Nr}vMZ`9t}CZ2zpJRL zxT~bAv8%63*`@9p>>BP`+O@T7qHAZ@?ykLE=ezEAJ?MJe^|b4G*T1sqvY9d)nXL?# z`N|??QL<=RjEp5KlGV!UWlgeXS*vWeY>{lKY=vxn)9*-_aE*(up+*;&~Q*$dfg z*<0Cr*~jjg-7ej3-Jadv-M-zl?v(EI?#%A&?%eLm?$+*(ZdrFvx4e5!_ww$Q-D|qn zcW>(6*?qG6RQH+g^W7J_A9g?Ke%}4E`*n{+kA06rk5dn>$F(Q4huy>JN$E-J;r0l6 zntNJ%I(lS1y*T~b&>htZ3 z?_>8R_Eq*(^;P!`^bPlo_L=&Y^sVjN)VE{S{aFuZJ)ZS+*7I2}XZx17Qg>y zeVg?Im{n`Q5=;j(feo+&_5cGMfirLgZom_G13wS|h=2q_Ksca+D8K+QfCb_~0!RX> zfD1A~Hpm49pa>L$5>O5*fe?s*7)U?^Xadck4RnHTAP0Rw3DjT^41?J~3v|E;#=(5B z5G(=9zzVP$tOXmuX0R1ZfE{2r*a!Xqhrm&A9GnDyfwSN|xBxDLtKbH>4eo(S@CZBw z&%rD32D}3w!DsLVd;>p~Qh?^m8nXuGE14G%vTmE`AVTGLPb^4Rne+gRlF)um8?otaaEbB zY*n7BKvksTtIAXrDxs=IB~dk~q^ed`yQ)(qQ}wF)R7#avHKZC*X;gDmdX-5vPqk3B zM73PCO0`zCUbRuRS+!L)q1vh1quQrBpgN>FqB^EJsrpNGMs;3wQFTRiO?6XsM|EHI zQ1w{#O!ZRrkLsQ3gX**DKh<~DuYN?oW&ia4nf*5Xw*7EFrXSmn>&N$d^n3UF_7nPv z{iObo{;>Xtep)}hpV`mqXZLgZll#;9Gx`hF1T|4jQirI+)l@ZI%~Z40Y&A!nqE1(5 zs&mx&>LN8?U8b&33)MAhiMl~8Rkx};)G~Fix=*cCtJOp5*=ns?r#7nRsu!pitCy)) zs@JI3t2e2)sDD@QQ14dnQ~#kptUjhbss2lSR{gj7lKQIphWfVpp8A3MvHF?%rTQQB zJM~BPzv{2*9|KbckONZ(W(-&l*bdkaI1D%qxD2=rcn}5(kn8(grdHvIp`83I~b@N(Tf34Ff#`nt|m5I|lw7xH<4>;Qio~LG+;eAZaje zuwYO;*gB{j92s0ZxNY$0;MKv$gYSo?455Z_LjglEL+L~LL&Bl1q1i)=hc*uVG4%J) zy`k4bzlPDne#5lkq+#B0{czu~et7lpuHjR|w}#≥=_L!H@Wl&_}oA#YSfw`&1{WUGe=|4jBDm=7HO7hR%ljh)@e3qHfes-Y}0Jl?9%Mf?AILB z9MK%t{HZyuIj6axxum(OxvsgTxvQDfJkmVXywJSXyw!ZreA0Z;eAWEW{L&({mfC6B znOYmIt=3+P(PFhYEne%P_15}n1GGdfNlVd&X(P2!T85UXWog-3jy74Frp?f1YxA^) z+G1^~R-moYR%^xDI&Gu2S=*-V)OKs-+CHsPtJV%_XKS@uoz|$Gt6iX7tX-yEpU27zZmw>DZn18eZl!LG zZoO`k?l;{w-FDqB-Co@R-67pk-3i?(-5K3^-9_CM-F4k9-CfZ|o)eVx8h->h%bcj~+Ka{Vm5NMWzd^rQzg0h>->KiD->*NYKcYXb|5JZje@=fve_4M`e^Y-)e_#Jl|3v>> z|4RQx|6cz||3&{z|I=V$fDGnV3WJTo&VV*J8k`NT26uy(!Ph`A5Dg?lh#}lSHP8)A z1IxfRa11GibVH^g$B=I*GVl#$h6;nwP-Bo78VpiHtD(anGxQqz3`&FAFl3l*&>D0G zqhYRLfnl*>nPH`2jbXiElVOYDcf$_DF2i2K0mC7~QNsztDZ?4VdBa7+6~lGIEyG>I zq~Vd_so{m;wc)MdgWl`z(`M6F(}ZcKX^&~Y>7eO|>A2}n z(`nN=(*@II(>2pg(;d@&(?in}({s}+(;L%!(zO{|8OV=E48~ literal 46049 zcmbS!2VfM%_y5e!-0kh&UG8>iqKGkqSP;_aO0UvMfY3RT3j{(EQUFC{2NlGEh@dp- zpkl#>ibxd|5i3|x0eeIJ+AID)Gkcdtg8qMml)IgI^WK{`ug#me2}SvNC8f#9M+hYv zVT2P8@e-YwBbr2tvnS!h*~urG-UVO$sMWj%JsZj0=YbWb}<*70u}q zYiJWXZ|&>EM~pFjy0&ne+v#4qj~=8i(ZlpT zdX&CTKc`>NFX>nG2mJm+^J^iko>pILtR-qqw5HmHT666(Ek$dswb9yZS7@EJ-dZ1R zkTzHws*TacYPs4}tw1Z(X3#IRIof>fMs1OHm$pn>tF6=S(;n6y(H_-y;MsodIqeXB zAJN{_-q$|RKGc5De$;-`{?h(tlxfV%3}&)AtS)QB&Sgzmb9NbP!CKQVSVz{0^sJmbJ+rR6I;TTY4zA@ttnfhHD{aHJ?vig1l!4W zvHk2V_8$A1eZ&6ZUY@{hUYDQ68}Y_Gk+-A`X&P_CFXvbAuDl!X!TaMi=L7f;@C4}>o_d~hJ&iq0J}}(1>+R(2?CtIC;~nH3?#=R!@J{efjL!s{M!)b*^Iqk>+AF*_ zdKY+a@-Ft?RpseA@BQAbT0QUMT2t>ft+{uHcc<3dyWjhg_mKC9_f7Bn-jBVXct7=? z^!`w}AGrC5_b;94TsQTG`bB!O-cG+lAEl4hbM$<@OrNe_qtDSD{W|>?eVM*oU!!l- zH|bmS`}K$PNAS*e{b_xtzDGZ#zpNkDkLd5~C-jf->T~@I{YU*LeE+GR@^Sq~pW!oo zb$oSwXZz0aUFd7(yU15Z-|oB2*Vfn0*U8t}*VES*ulxCi`?7pvedByl-(q(0B;nj~h=I<}d z`|*9yIAXkOd|-TNoG?BzJ~qDQMaDPAPx$#WzXMQzG5$7AncS>zHa5>QFE+07 z{X0(^>DBU)4*TEozvn-SaX#{Y<^S6Mi~qL(#VF$go`4<*O4xz=5_aIcKw_X-;G#fs zASG~lpkttyYSlN;Kadd^jpt(mx%fFTFey+NC<@F7%nZy4IDz?r8v}QMr+){Q1nvyn z6<8Kn9#|2$JFq&iA+RxUUtmjMYvAF)w!jmC9f4;8djtCdF9Z$*UJkqxI2ofo5A;jM}x5tc3ar z4H6n9oSTr8a6!V>ghvy$B|Mezbiy;C-l0dsjl$=K8|%^VdEvzH`QfDS1^#B?CV1U6 zd||j*_@Z#*;DzCf!XvP(T_+yvc4PZI zJJCL0A8aSt7uZegruKz)Gy5X@V*3*NQoFf*ncc!}X(!t$cB-9br`xUU)^;1at=-OU zZ+EaSx391}+MVpqb{D&=-OcW9_pp1~z3kq0ANxwXuiek?Zx65s+Jo%D_7FS6&a{Wx z!|dU9mOa8AN!!_@?9uiZd#pXq9&bnN33j%fV@K^=d!jwb&a)@mQ|x?us$F0g+C}y> zyVx$VOYJgyx;?|5X{JvlrR7+l%cx>?QV6`%e2Vdzro5USY4aSJ`*ltL-)RT6>+n-rituv^UwC?R)Hd z?fdL4_Wkw)_Jj6A_E!60`w{z5`!V}*dz<}){iOYrz1@D=-eEsu@3eQ>yX`&pUVESY zti9iU&VJs0!9HNWXdkp+vJcrW+ppP&?YD+DDJaX&KNhx0kR*^02@{LhqzXG`S z0Xd5_BxjRzNF#DCX-v)|iR65eL@pprNKKt`;Yd~!NS*~bMi+653FJ3Js`SRMw>j}Ff($SIsrGOS6q98Ust z(STPgU#X$oUU(Iwz#XhXHT)|f*s~ZI&L}LNf<8S93rhP(3L+Du#UlVOI_)+#cRUWU*d#}L1ME*FX(Sz7 zP?Ssj9PY4j;ktbaatmE*KtgG8VSawJ_zlvAv?c9G`z)8fXvs+Tj{@{tPui0XV*#kF zq_l9VOIuc)$Z_FtpRU~svkN2T#JoW|V)#y^GiD>^#Fwmfba#xdq}vA4jdUj+!0P^G zr4jHpBU(CiKn8xgJ$sV*Tf?16FVdUzAy<;Vq#x-|29SYd5E)E{kPMPZhLT}qILRU- z$Vf7Zj3#5qSTc@`ClNA%WRn~cCAnlGnMCr)WHN>1lc}VD6p|t`jTDm-QcB9mbTWg? zBv+BE$t-dWnN6-GbBIF(nMjj(VWE~+3$W54so5?NYROIE3MDrIy? zw4|u8ASYT3LCYvBiWX1Li_QRSB@B?`9tCoOb1=&Ep81i9B@pOt(foY0?p_?3n0ZwZ zBn_={@{}Mra6+6qhD8I;*-l+2U>qSEfX*F2WFr*BW^xZ2XOvBlgOoT;Fkjj!XSgdh z_mR$<$rf@yd4N1f9wJ+vpp)Q)oUmg#wxeUscPT2$k0uW4-ZQZ%Qc@b7P#7uBNh}2h zsV4rT8r$4L*7UktfSq)?H(sK9bSuNwNynw65=JTa|xo7v41XQid$&(zto z-yz${(_{yEhU^5Jc9Gp=55D%2efaq-*-xIsY-D6dpuzCF^s4+QSSS~Mw4hY7zDq&g zR1jQ(r(N>%CKgDAfeBKRg%w)FlNT^xDmg%2BnP1$APP#5 zl&WdqoaNL9%sx}0&_-t#M+!=&=0SxQ7PxBQ74qr^@+x_abbtcEFiN$=A&d)KO{>4> zc6);y-iU#ZKrTA~Qw3e3I2!HCU${-*B5!XZZ>?wH#6G2Y@yn>9ptD{)q?vuN)w#JDs(Q;7lCBi~S9!e@sy!HARSobQ|`w0{Jnj=#=7_;>swLIl&Cq;cW1 zX3c8dx}6ly6eX#xAhqeq?PkvgL79cc#Sp#3J_RL3%8Els21N>@`EO84>Qjv};M1a{ zv>23S%3+-g)I3lR^=_bEs>^vm-Y9N5R70u6)E`O}P=n0hNKJAJ4bY&|6pF32bGeiC zb~VKYrABSCiq@s|Xnm!5E`;VWomNh>apC&2W~H|Q1#MF$1*vUYH&0AygT;oJ|7>~= z*+Cn*i;bR38`JY>A~k6ey?{1RQzj>7VsT+vK~A^A{6eW3GodQg-yjZZvZ%1Q6w?!z zLP=$FC&jtgxd`Z{+nk>2Y?IDDGA>+aKw)B>Wf{?Ya4ed0gf^oWfmavPOX#JvIlT;G zET<2@a@D*gbs0p6bBS}QDQp|~7*0nG{$K`7>O<7ME`a*)(z6h*S7taU@4F28W1TKz4Z7GbY* znUg#$<0`1ysiU)9PeXM$Ogqt5fZmyQQILmE$}5fbO>WWJ{nw6!nUn0|)}5xUr)i`^ zWu$u2UM^Bjnv*KWfC7nuFXwW6h<=w;9DkbJ)KD-_HG zWoBf8T(5mF-)a@Wz|SbDSPGE*OxFj1!mQM`#Bi=q4En`sZ#X~MKA4uUEwsO#pWW3g z$ozG5uG7)UdMk$Z4bmf^^JoxX3+PSc9*CcGM&NHNjnN6q{cm8|Ps6;X5LD9MRT0|d^ z{u5NqR#4IrK)XnJ=mri#)1*Vwq$pjP-WG~RibIW1Zll{`GWC>#(!;q@Lg`zxT8hw} zAXy5}o|y1-2SE@X$W0bZa`u#Har>GFx}82BL-$2BP%o#a9H_TKw^d5>#ME}Jq+(3R zpS7dAiM|>`_YDPQpff;18B`m(Z)1KcquU4EPai%gFN)}|y;?c#(#koN+8Pd!9Q)l0 zolHNdq?IG|1layDAbv_elbSSRP(D=EBxt^9v1=}3Z0U#T=r0*_{;4ilRX#E(V?fI; zO zSUZoryTSG0+}G!8NgK2z?E)2&#=L+o#l?}Uqz5a1D;Z6J@lHu|9p!YmAFXE39ySZV zlV`sj7Z+Dlv}W2x+Qr%>avsPYXRH%(#yF#$bjgVdm3oO(samqqro3(?bEKteX)$v& z-WlhbqdKL~MHPy#veDDpDvOi?ixjg_S_k<~|K!Bv7HyN&Kl#ulwWHQ)qt?kK9sx2i zs#)|FYTaWD?4=mk*~yU%L^uIACPg`7iW6rxHd;TeKNMqyv6<-PLhYqv>BKz{nK~#& zxBz*o;7y)%LbOcfXavu#8N;O-jGJI!);ltnb?O$p#I% zbLp>XQ3a*InJS?a)}l|e$3^+Xm(j~v_hZ6hW?l3y-|aCR1{2CWAAXD zmSaD2rscgW?VWzGAVp4k%`953T^Ac*o*JOUDV772BI>|Gf}U;LR&uD#88z-o z?UopaZc{)UXO09SYU9un*ucshDuabf9}}N$>08udAeL#XV%V-x1I=`1$bsC&mnMx^ za=Jvf6--<06S_*<6hrr31?3uNmV{D^n;p>}gu$+i?$t2I=_9+t^GsB@maFX8=&5q) z*FOB6+7o1z_LR0=ds+=Y%~^y9z;JGJZa>qsF4Oh`i%+#@%gx73=USMLaAz+(b_lez zQtoV(BT&shR7(6nZ19)V;Pd3T^UpNya_x<3 zfW}TAI($;J7#aiUr^)$q@lT0O--p^U?YMTrjeN9^w2!qp?h z)hoD%UCT;K3ky^blL|uu%N;GwE6mBu?t{EVG2oy_r8a7ic$4nYd}#w1Nc8`tyFWP-AOO27xN)3Luvq}!W`b^7lHM?9cNBUX08d&PAl*)f}2O=5X$va`|I;+a92(VE8W0KS(%LYp%P;LV)r~1=SPaK!qTgF z;dYt9W^Q0J+2B};K=QVnGc_-OyV>m6Bsyvm?{FTHleo1)Gq?Fq{ui5v;Ip!3ei)%; z`bZ_yNJ)3S>I{JayII;4wonEJ>^8QD-5w7NoJSQ>k2y~|+ngujo;+I$Po6Dw9;@of zuSf8^j;(?x{|LSwbRL(UJOo<#)9sW=PmB4}r#aGWopv$XAPG>8^maJXPdVvT{9Ku> zkrqY<2I-0?tz#DYD3O_d6*#l|$Wpe2-OnC?=sc*%dYC=J9);&On?1%JX4@dFy^+9F zTUXGACCXCZ&0+C3A&oJmuMz^g6bE)YUpdb@pE$dnuWMUgPb2cLyu5ZIu7^@7oH`XE z*j|M|Y&V&|0Z2T?9tEUN05R)*i935vV$YsuFR%mbMTGxIsj!1|BRhoeSJ*3BDtn#1 z!49({>`ke`WqPm#>!*8Rw!~4!UcFSPfQQ}6ic1RRHej}!F>&@FyKoHOd+{~iIUeTD zhn7K{k6n!3mQ(o(9%a4Hj=EfA?*oIA>;v|p!s8e_4wN5ekFrnLr|dJ*g0^E{urJwH z7!1i{H}{({2~l8<)b&{5Fr(3l5Pcw#IZW}lJYgm|8_6qxo_HS|*bNRGa`rh#vGr)! z&IitZth8A&)>6`vnhyQfs`c#Ial}rtZzXfs7O>|#_B|3U8-dtQ`1uR_mHozkXMeCi zT}9o;S2?79%!NAc{!z%FoI0pN!BaE`bDlPEUUBuo7qhfoz$%au=hI8 zg{kw>n_TBUh8zPoxJlcQ7CgX%5H$%X16BuH9YC^Q{}{XA=g24J>?(1dcZhS)dGWA> zflPJ_9_AL_I^ewEX1_nDESB!(^|-zf!`0^vq#kNmuDZI!b!?8C!cgu}^*5DR@P<+> zzH;_ESxT|-bJ;)Z**~O%`|3P)Y(2xS#yk9cp2RQUO?Xofb|G!ZoAHYvAU*k|yg6y1 z!2!dzM?A?_K9bxpiqY9XN=t5}EWdP61Ux8?6irgw70S~?o&1IJ?>O(S=U4K+&Rfpg&Jif4p>m5fA8Y0?A1IUGd=Tff17i9iy}I^9?4MX3 z^z#g!xt?drNeA2GB3iLDN6%6#DkXYpa+}#B5Z$}W%S9=Ro!ZP!@ezDvY}YH6qLxyQ z{Jw;y7N?9MRj3eOoQL@sJ{D+=!x9+}8SY(Jno(3J_laCpq;f(hoR5ajf;)FvtJ&^w z+3di2b|5ZLQJ%Yz=eh&S^_!npB*%R(rjRPHd$sE6db#R(K34s&>~}5`s5@YDb~v9p zpF2p{$*@;uMir(hKv`;$(c^Lo<4PX-7*z|rn3rtgC2{8%Bgdbsji}2~lA(&^Pf2iF znHFj*H=+SSpP3W!UVJ|1N=pPkiWza@t654{8j#%b4pM^;ez@EO%OCJXnjG?611_PiGp4z=tY9I z5VV7!S%QuhbiAMw1T7SFnxNMTI$zM61--R~Jpxd&6}Y_R1|j@i5c3zta{ht52uhXH zBy&Kq*>4jI%TIGh6`URXSWN6ba^V1BrT}?rlf2LQ_R5kcsOFq<(nobKoKcWp7|Ds| z!~{^T7+3OBTGh5wp>TMHe;=deM;8ZhOjkG^L@T85w$$89FFEEg$5l>El*b}yPZJkN9R;CoZIoZkcUGm` zhEzN0!-mKNZ>0%$9rgc2_AXC)b+SQ8z1qm$<7pow`wAChtfdAD>4u7IX`W)cFRh@J zq^7Nj?5?p1@8JSDM?q**8`)R#-Brmx8_O6I4sSCrC$W3KN|d{sT>pvkJ)WV}DF-p< z)<*e0&!`yXV_l@7Kh9HlpYKjM7JF<yu^JfpNCe}E+x8}~nvz0WhfI@zEEMGQ5orv094Vr0*8F^0lyrjWka)p4*lscFjM zw}HslHsROD$e!;4xl}=DUK`oB@Mo)%eF=0mv|GlENb%G}NP9eYBGrpK_( zWI@{+s=CG@)DX12pzQ=r!Cx&Aw746imCBW1qCNMb49;_(XN%{4&jX$ZJr4<*DrlOZ z>4L(Nv=+3DEQW*klPXncDk{ie=i5Qr+Q9*MTh)@4SCVm#8nRXRR&`{C(@pf4nC84qy7JMu!iWb)r!F!%kbnoyyl$`-Vvvc-ec&rX7NlCiU( z17mTs=P&{!&msAK)=@Wbt{ygf-Ub>wJnwmqdftx*&7Na|4p4XjwVr}@7PLP?ln(^O zmT{HH7ZNOe)=F`x=OZBbiHt*&x)q^biUO;yRRT-T=Wbx>`AX1k0DDl-9+l!n&$oE* zfS}#01dMXxe~`ogwJvZks)ux*U)+$+^Sc|;3EB%_AQe|uiLN}SytHz3<<-1Q(B6Xf zagM~I9Iw}FRE~1Irq?fMUqSmhM-ZB{mi~2lXyVdeE#inVMU|PdDnS99uIJfL{CRI( zHze@Fxq>U@{vYCeE@(z9DuB3Fi3(JVQ3V(>3+3P%#q$bY_-W+;hf09M+(1BPYumV+ zC#_^Wg+QPNZR@?j)m`2TUC|w()XvCqeVv-(`qOdeEWXiPrB*EBZO&h)s*Xm%U4$0y zQCy6~bxq@@x$4f_+8uX{8h30(b}_xRT*Uvj@8i9kA5a6kr9=}29S0{hmOU=1SzjOY zVrNG&XM4Na*x5w|6v+@PpoQ5~MdG{YpV6I?$vb1<5_^+G(UxirbsR#(8sng62E- z3pyz-_LIE%Y{~{ajC!ZYXZH#^b%S@Rx4;YUK2Olef=+R^)Xc;~-m(}IXS(zw8YogM zKt2TVWU3TDm^#d1YnY%~jGi}^WA-{OkWvMqtTrazz+bA$#1aG|GMQXB6=l$<;Hq$Q zVq^a+BX0#G4|x|=X5@6qNRTtT$~J*_33zxXcsNV)a7LVm%e<>350`+4E9Eor@R|+Y zyS=NuYXqGs=v9JV?QA(ij@!FlniB6OndA0u_TJ;YH=g4bH0Dn@f?hA^TtTmkC%U~4 zKrwnZ3F^q2c(=B_J|eo8y^oT6R9J?{OjJ&DyKa81X>RY6+QnWKF3B{vp!1wDg5Gdi zqPwPr3~LG#SkL>cJB2r@DO^zF6l(pv2ly*$3d`5)O$Z^8j#d>rxzXa9rR_EEYcXj% z?2dA)ptq=j7P{IKOSYX`N^pi1`%Vngqb`g^3c&3(VoD?4u)QL_xG7Eyy~cob@aM=J-xo( zK(?g63Hpc7aDc%hG)-u_&=Q1(vd6Q8cDB$GoviY8pr1|dsJtbjpQAU@DZb9r6ZP}; zB>e(9TW_MD$F}JxTt?ElcN7tOaXkN4p1(y(^hS@(3dR;E>pjU9wg2$~c0gjgA9@Nn zpzG;+D?OdPp|^2;vJHxSaA2dL`_#|xV*Z%k9xGLcqq@<_Iu@?0cho!So%Jr{cD*Yt zCQI~gWU=0z)Yp6HJ@sDn2EDi5hc+Zj^(*zhoa_C7-T-|dS)~ugUm1EP*`v46hXLU% zeFWUXU&n=!s8$68+1O^6Wy>frS9<|qk;^kT#fM}&(!6C;3%ZmcsUI!LyBZBuMjNFG z^1YJC^r)IpectrOBo@8PbZGIdg6m!v3Dka&I-Jp^1e=OEShj+0 z7xZaCclffh;U-}>)ZJpxcj(Wo*PoGlGElEoS?XfCOW(a--yKg{#~0*LeQ%{{-4~x& z@&l%JzxI>TLN)<#}wQe;Av`mV!R(&SU*-_n=3$9kz;T_*nl0)38d==LCHooHj0j&L=qJIh@AU7%t{)UbUQFt;mXM^bZ%9rY09Otrb-y~sm6wOX z0%F&pI%j^C;;jFsuh##N;{1{lXF(5wGd35eNUUB#|BH5^9dIlqP5w)5*A7*=|CwFo z^!hwLuS@SMg1##09h_eUI$+HS(S7SwS258vD*uT~f&&Un1we z^I@@n@?GFrNA~-gs{Qe9#V8|(Aya$uv7KEMg5HU z4l5oa-S8GDxS-Q#x$2)vXT1q6B^|3c1%0p(6;Qdl;jdyl9d&v6Ro<@P?!~@KT<*Rj zC@jwo<4o*q=}Y#d_)zENOZT+`JKHFBzMs?u>nN#fSn~8Jc=};d*WNJ^IyQ7@Kb*d* zEEH0c{Gfktb)AXsNsr&zhrd;1d zm!>ZS{Zi62IySPRs*)b@<@=_(dPLA~1pQhnk`WW*ZcJ=7`ikXh?4I1B6_(*x<%ugx zP`+*Go32MU>rvl~_&%^YofUU9ROBI(M((br^l(zsadx&=C&xG2O+oqQ_?)=IuvXCj zNhfE>l1+mCpt3K@BXEJr4Qd4w0zGS1)m`UQ72)Fug7OD^@CVlEec`e=S@GPM@+y?) zCEW@)KMNnDt;%wxr6FgQ+B&(0^Ajo#NbubX5x9-k^W7eI?h)Ypj#B0(a`*60H|zs` zZlXrzY--xGmcVGxcUKJ93Ky8%T>V>xf2V2*|9oroVAb&NFT^(pRAV!azz^P&+GSRB zwnma)COOWqf2Jq+wn&h^2P!rKLDN65LV+hMphJjI3Ik_ zaQA^sw#d0~8PPU|N6pZJz8x`WyW(8^O=za%gyye}6VK|QDxAR>s#g@)zL#QS!d$2@O$LTuHKxx+U+xCE`M$Q03dzQr(XZn>5(D*i925e~ zpaK=D4gL4^a8>jT#HWbkUF0*OQN)$V}5_J-yBNcSED zxx=T)gj}th4H?*Amctt?K0LzsdTRLkwQ$z(>2g1~V(U$-i+~?FO{M577Kl6ke_0@P z4F#~$0+HE+1~qKFDaopDV1rq%4Wm&UX`~q%Dx}YG^-Z!%HL{Q?$+4ADEr%r`8Q5c1 zaGJ#7oU4E|u8s1G^?Fq)Z-gX=WI2w4%Oz2z#vm?O;6Ct(i@}-Zw7vm%wW?r&u=AYs zns9GNRv8_@gDc{=pAY4bBv&*}tz&F?R;sny5lfS`21*8M&f**0^add70;8v^poDgT z%-W>ezOM4vww;H1mc)X>(!|L0NM63QL)G$3#+5W^^fUUaJd@T;=9z{F?IPrvWTAaY zc>#+tn6xl5kYmy=b|7YhV+kf>7@5D`7;a<ZlxoeD zxJz2y8m50EHP?_e;!z_nPU>YCRFVo!tTyFNFWc46F{UUK3XDRdNM@*vX+{CtW|Ru8 zWgMGip$!sRf1zC|aI~&+?n!!M@>|(UV>)&NjRF8DLOa#Ee=Qj&;~FCiUvuwV-DVnXPj!tH<47&3ODDZ_tAU187Iz-1wiR$ z1IMlO!A60e;aO)aGHxe(jK#(s`UqpG%s2_HH!x0uL=gKNnR}7=V-r_P6I!~^S_=*4 z`75Edb;iR&$a$AnOQ1?nCPz^JROyw>T&q+Dq+zA8N}nZ#p-p^i7>?e9YEmyth1j|& z3Ms!TMS;yhW37~yb;f$3wG$e)JKw7*G6hj{jLpV9#=XXUkevHv(4pAXA*t)xQ75U} zqC*c0tv#sdh`1wDX27bsk;YaTTp5qXf-4Q{47mcK!G(CDN?cWAETvs+%&8oDj*;_{ z{!XP3T4hFnW)w%+^ z599%-+Kx9Fhm4o$31bfVK|5)@7C+o%97YmE>mju6@^`A;`|`m!PUQ5nDIui%@R0GQ zJUnE)lhg&PMV=oGhu<*X!;~B~-bXG>?cPgH3azKmdZDs8V@6(S_9T^7Fpe3=*Bi%W z-T`M9+%rF_gPUI*RY_vpC5Lm6wDm{o8*5`Mt49)^8g>7(Z45IY5FeZSb1V2G-yjr=`jP;2M|vys2t$ zLixODDQUB3SJ#`rL2v$H{ORh=OsO~j653Gb_u&JJWSMd0488FWX<<@RliG8bTrjhQ z_Ge6injTpK7&5)4jwXg_ntn522F(OBWQI-4w9PsuZ1ixUO;`OKq0JTAa-q!@+BTs* zDYTb`hUIZwXfuTdf`6@{tk3w<_>%@r9B-C;sOGsqRN@Mpvy{;s5u+OBHZ@Ht<8=8a zJbh*In0sbtnj6h)&CK)7BpNZj*qcc*o0%73_eq079VwUCaKlMg>R`z`nHeZn#B39%6BvC~pywmq3){qB5>j7J#?7I$B()@}(_`)H(C%QTLFJ>J%xE zzotfJs&qkliwLT`OOVQJkuMYD$YSTX&kN*v16&uZo~xdD^3-|@b&(2^3CJ+C$cbj> z;hdm+yL3`vSqbhkafgq0c87P7orK+|Sfdi51V)Es6GH?zzU=18H9 z78*1cre>Vb#tSVXvM;v=gZ1+4!Qke&OAZhwnqU*!Ppu3ROT&dyh?{2yylq}(UTw}Y zuQ6wn`sTId2Xl^_kTnI_holfh0Y$(iC6N{r3gryOU&_CuGjWX=vUR9EZ-EnE6H6z_ zcjIKrsaJWz>{9GdLA{p$>)eD$vpUV^Vtu`N!+P@usW8u$hZ2;q3@DpA0STsxyB`L( za2wxfF4$l$VCtY3Qp;{Tm!mjjRw+e6dz!bDr>wOym7yqACp(a;J}vx`iyrD@iMxYp z-sLWOr6y*mRgcrDu@k$g?!m}ZS$$q(z@cDS=G`#`vNjIuxX`{=uxhR)*ZP^8OkKI% zxyot7EL{yvrcS>{WvaVotOMqQF|1+erK(cu?iw}HwdGi+rMjot&WyF+#NleC_O{1i zL3=)`U_Dl2umG%gnY1d_@Xcn)V8M=WdM9?pnx7$0@YGC#X~u3YF<*=i3%xDWu-Ccj zR-VXAZL97ZkTIWBw>85Go4CJ3k?=+w7Bv1+1?#TbH1u20&_*>4eZADsxKJ?tzv<-n zp_4x}kD13$)5%|&UzuMcQ>fhmpD$e^M3hB(zbJ;1({qfoCm~5b}rJN$}T+PXa9O2Wk>NtWl0=#DA74@8c@J zHA!g8V7{I7%)VV)o?hvs7MbBsl*9Ow{0QV+(RYNl0&IRnXe*(j<>h-oG}XNkEJcP- zsYoFn3)|XpoP+;J`^0~tznS)#|03;E80_8ti)kb67(K^-iT1JoQh#&*Wt{t4_*?pu z=?U6J``(}GM_rfc_qV1ITm*2EbJ)E8(SkCWNyjhu9wivvLB&xNd*EbVkDQ6}qAhi- zy+qaQsVzL>7Vt2m?ZIyBjuJ3|-#k_Mn|0A?DLR&4})`x8$ zYG^HZ0c~@Q4rb5-`U^A-__sE-<^Hy?*V7xdyv)3 z-x~z>@n1<#_%HDH_Yd$71hV}LqEibC^0KjytqgViDuDilIdYc^K7=9!zt!zDQ2zr9 zl#2^4#M{Dd_0O9b%~9_Ib$br3o3jb*fY6{3*GW273T=bXHVbXjVSiUr-#^$t#Gm2M zgb|7J5+?p0p=}k~y<>aj7fwLt0T+(qHq42!)B675^0dCt*30AiiifhNiX*1tbXvUC zXt&iyw-vSnWh;eVY(}hcMTJE}i{y;BhZNmCME?YTv-P;`xPxM*KZmlB151$#l%ZaU zJoJY!%|8*5u0PqI=Z7b;p^wI8V zjzoA*HQZ>T6hMOi&X}ITQg9Ujoct%0o_VUmaHJ)>MV@Cg9J+r^3>G487ZzNlg9_G5 zwHOZnW*ClOHN&x88jfxM)o}Q6$C&>i|5pFQ)eVP#yZ>qb4rw^BOR7C9H2n2qjfUfC zp*=6O=i-LLzsvtT42OTK+28*Vi~4r}&U30e49Bz5aNrd>yl{{I1^)s6i~fWDm;8tP z(9k=C_KeVW3T>Crb_;Ef(Dpi8Dw_@1RJbK@Dfj<>nvJw%6(Xul6`7^2IYZ|^W+qg% z0f!K_)sP=mvmf+-7E}LU#%FAs&|U$vp#ER2Q2%M}wKo4}^?%a;Z58z&^8XlD|IpGx z15;jCdKn{wiLXH={|D`RGNyh1P>c9WseJ=bjsSKzwuoPi>&GK8{TR^v83F80ys32J zTjg3QriubSsfxZ0m;pa96588Bd#^TCq(%>fDve!Fjr~sbvEQv(0lEFps?@)+>VLF) z|M$zaK^kroMN&dt%UQb!1e%!ceunD=V<{a&%#Qtxc+UAu`oJ4#?n*=e2}^ek4;R`A zS0X-&Nrc>(b8i{LO~Uz8Xrm0Qet$V(uB zr4CJEs~5u3Ccdduv%U!An|4)w@dIKu+=iuqN;KgsR4>n$x+5U(%|umi04m;y(R3lvPZpcPixu{H#UR^mHk{o!HETlmPHz)him^u7wi})6o&u}ZY*}ExYKF_G%g-E$RT~x-m$LvM-wnJTI4VJ&BSEsW-l^>Q%0MIVfmCaO z50$BakrS+;U}wklfb{>5_`Axhk^O@MpCAm$2z)Nsa4EZ61w*Yx*uBjvF|PpE#i>i{ zrP_()%RGAf%7}aeZk&*aoC|I=E`Oq@2mUAdB+G2rd6H2hbEVX)Qk3#mAmmrFP*0cr z&v&rhx^ZL5VH>-828$8~kxEO&u;+@<>;tR=T`Y=u;9PgeyyOZ*&sB14Bb3tYLAz};Y|92L4U*i*H31iQj*T{qWl z9fMx21A8UdPj%QUSSQ(GbS_E>ry_G&g?zn!aERg(4im60F5t6eOZV)LoVptIi|0@< zOZCISMAj{izB-xKAN8WR;HJtjbaQa5!seu4J!I>Vx$?G(s?$uD26I#+e0)VO7iISf z&Z^BYE0dv849Vy|N~JhbrDPPr^13ReoNJ~xSmaVST(G__mf_qA@~X$Gget>+5X zKTeY?l?t%B%en%%5xmB2jyV}vL-VT8jXA-&ZgYIx$6%K{DG{-~+j6iV??$rFm?O!{ zfIMocl^@{)I^0J?t3C>R5L_%DDMdFzJ9)!3mLQ$xY6Ziklz zi`3Ym8cJ4ho7>^IVA-<6D0hOZHUn38Kxq7ut5GF1%c15WHVWxjf~T@-5AH?tkZe9t zHGjFB2>|sxo>}r)o_coN1*PPwCQygaTp9H#s`<8ZPn`$%f=zX)t&7zcE3jAP zi`5ufH~6;O<5$57WsecCEHhR1sT?dY`~XANl^9M_L;gA(q@rB~h5TUfV+9Du8(E1v zbV!>1s5(7abyR*R_@&zz;L6;_n0GX;&=m4={BjKVYr*f_4z~(6!|k9Pi)j3iSJeUN z>fq0^y|YxXt7P*rxv>ixtF>Dj{8P2NTd-L!Sj)vjUgsFisoI)9oj_eIh+wnb=Jm?O zHJYR9imG+llc2j@&Jt|S>0MB$oL7C6T?s+A3qJ8j$SyGntOj{sLLIjm=K4CfnaTuK zYxYb+L$?_|OXvm{fjTkMSTn}^6V7ux;3I|RyB)&0>dFG85*2u*1`?ViJhU~u05{nO z`ASq`ZlrhVbr84@!f7*$&O}Y;8Z&{vY#O-2W)I)V7V>3?9M(jaZ&8F!&q}QeE=vwb4{YL(*>E*8(b4<#2u}u92e@nX5 zpKJ!m34JNwj=O|6m=?K(AHucso6ImjsO$U%loT$|=h3^(x*BJ%`15eR&fZ{>vECOp zle}M$<@{;7+zinGe1us}cXZ$bzE2PF z-TspZn~$17en6k^|JL;L=MY4%=POJdm$|H@rk;+HrQ4jDa;H5*)}rok5S%5bGzQ|< zvvB0j`2{s#4pMzM|AKs{qps$`jU5TN5fMQS0=ahTZlfQaNjRW`?1#Mc2V4E})ZR&X zIZvS~W4azUwqJwe$vCy=bQIclStS75soBnh&fPe3s*d$KpQFOV#hBiaggkO*=a3r9AXP(61l5jH#sL!p(ZFkGU{eQJ2*pR zr3@;^hT-rb)~7s&1eMrO-eiPhiMZHErjI^_agp^7bhJFKBojf)rMHHR1kz$4Yr4EE z$8nlUj~F#!Jp{Y;-;U%|dw$v-6Yt1F36+i{?3M=-+~WudPm=xe%m%#gTU2P7u-(^e zqpz8Ji>~|54()>V@^W~0Y3y{D#$0@+MGy-CH)QHm3XiO0#By0q=c1BitNfN7+Z;u^rBYxQ)~kLA&~-H{c!g!{5u}e@Pe$4n~nraDVtE4awg_z49>gy4AEukU{Yig+Mgc*NBwNU*(b^eeqAQSE#sDDY|zo=T7p%Az7 zp{_;87+h5(B;jMB0N8RlCU!7}!oJ~CGX7Gi;g!GQPX$!{kmI{J4snoaUZZf-d%O9)Nq4n3?23V~ZsW;^h9I%<-XtKjPy>GvGQ89OEqetU>kFaMH$&Af<~vZI zVBm0a9mv^kd=Sb;x&{UyO%Daqak_p;1D zq4FMdaH-6ADcHEG4M}xq%vnxnsi&a_Uxxa_J#Mme%aLcXrEP$UuJ25Ssep=ul1I7{ zXnX=44htaj-@iJ&APTsl@Dy|=Z47M)=PiSvwU+CG;{?K!w=R+@q4uoC|9Spq`Iuv?2^h)T}&}*UBLvMr*hmM5a z480Y4JM>QI-OziXqoMahAA~*(9Sa=~od|st`Z)AS=+n?=q0d8KguV=Y75X~#P3UCk z+t7ER??e9!{Sf*w^i$~P&@Z81L%)T75B(APGxS&J@6f5xKVcH4VJ*zUJnRX3!+O{k zHo|7u9}a|r;e>D~91dGyJ6tDRH(W1VKinXER=8mpvE%K6Ef(w!!IlUHC+<$c?hk7!Cn*Wb-~^c?66=*1TF+(ZwdCcVDAX_ zu3+y8hS=$S!9Eb|L&1&-hNXT&u#W`$Sg=n7`&6*c1p8dDF9iEiFi7;*f_)>{Nx{Aq z>^s4}7wmt6{UF$ng8d}e&w~9TQ1#1hRoln%Fst&?Qd*^?RC^~iJ*CptbPD!7D_WC7e}9 z!}YFJ0H&s>JM^;V(0RcaNUrd94#ou-u3G}$+TB7OHDeM%KjsjaJkYE_*hr=gt! z7FJ_YYU@^~y%pzM1u?jM3`14$4PC0vp5$8<+|tklACFK0Lb0|gl&YLauhReYX-KbO z8t`7#Ua6?pz^Bp3-c?3dlUSWlNI%}HB1MpyYVQE=Dnm$iRiRzVW%W*}X>#H!%iU>E ztBbefPnChvPMgoP>V2@rFmlyZ(mLg|R;S~Kjwonxmqb;*r>2cUI-nHGN@jCV^{u#B zoxU{5g{q88O;Oe+97%X+1Ah)me2%(t6wZHY+!BQ+;+bcYuc@&J_rwYMraW&Dvr^%y z(n=j<3&}{nLl5E8ZnNp#hzHl>BHk5rg?BWWOmFiXLM-@_zJTu|1M~(7w-VgHKt|#7 zai`-$6?4e-bTJ~ntLQ2dcYBb50h5eIOgP*>IhaJ|!O2`s@=>BZh)g7f{8`*+yN2{J z>*Aox9#lANK!w9h^DTam&Nb_JZzV-^2`=Vd&YvT*bejxjnIzW?lMJ{&1{o9hfaD>b zM1-igay4D1am2H`$PIKUKGSy+ z8A`_C?v=XyReYLoi7!m=#AU9}7+FNXNqU70BjXY4u7*>!n+C}>h+$tOSNcyP(!GHs zli_-pT&D*~gcK9U_!DuajVr~r^8=)e^o1+75T8HXPoGaF(3NJ0Tx*&nN^X(yE!|Al z1_sfY3G?_)-1tymx@Tkre*w{MsZPlhxLry7WikT?GUkxpq<;eLgwW?XFoKsTr-Z1_ z^6lu{Ap_A)(!IS zuQJ$`ZrgSlqQX!ka=l)<%5boMR59g9XA)d2+y*R@`9*Nq@EOU7b6I!kuQBYRNKcDF zNE^xzJw}u(<5cO+;WLk=*Z(OTwyyF+m9=h&E!k-4gW0;K0C(nuzf=hpqd6moP$Ph%6pXw}KJiPi%$j@*dEAU~Jyp) z{z(TMzfds5?=Rrvo*hTV5BP{?_{OX&qIwmv<0As^hIfq62$8wGL>S@9*{&;#5093P z?P9?Y^uwbXpxjaf_16jp7qlJLxO1-h80wn@{YYLR5?j?)d$p>q4%X$?6;?;9lhxVk zVs*8;S>3H3R!^&!)!XW0U1{~T`dR(00oFikkTuvEVr5vF)=+DhHQdUwMpz@QQPyZ{ zj5XF8XN|Wa)&wit%CVwWt~Jq`WaU|tttnQ%HPtGx3auh*npJF-Sfy5(HQkzF&9ttv zuC``b*I2WyYppq!V+m`nb)9v+b%Qm}ns41`EwFB~Znkc*ZnYL#w^@s<+pWdc9o7pk&DK5Ez1Dr!7VCcN0qa5QA#1Dk zu=R-bsP&ljxV6oC!g|tr%Gz!{ZSAn0v36R!tlicgYp=D>de+)+J!d^{y((3AVe5$XruCNfw)KwnuJxXE)Oz3g!1~ZSW*xUqSRYv*Tc22; zTAx{;TVGgTT3=aTTi;kGt#7UGtnaP=SwC1mT0dDoTfbPpTEAJpTYp%8T7Ox8Tc<1( zC)(83Y=#RZ*k6MEE!Zi+{t=uAP6gKlXM%IVJ%W1$*9G?pZU}A)?iV~Dcu?>J!9#+F z1-AsZ1+OD`UBT-KUSIGAf}bULL&47${2aj>34X5NjRikX@I=AS7d%Pu3j}W>cvHbI z6ug<>7YTl`;Fkz~so>28zfABJg0~brS@0CWQw2{GJYDcsg0~jDjo@tsZzp(r!8-_k zx!_j_-cj&Qf_E0Yi{M=a?2rdMlEBJMSUoZF#g3l9tzTh_szCiGs1ixAETby?Uzg6&sg5M_iBEfGLe6iqn z2);z{rGnon_+5f86MVVgD+FID_$tBg7JRkfYXn~__&UMY3%)_{je>6ye6!&92!5~N z_X)m5@cRXSK=20ze@O7Hf!JiU*yWmd?zC-Y51m7w6 zF2Q#TzDMxAg6|XjS;6;X{ae~{ODnSIHgMV9tIOS|0Crde#9#p+wyV-|~A^g&De$71JL z+B}QCY-v|m^aG1|ENzy>MqAoQOZ&^xuC~}ji+*X*J(l*pMPIV$F-ybWKU>;lOWSE_ zOT%j|*3_bdEv=EosHJVT7_rz{mbTww=@#v8vBnl_Yq5|;pR%;Ame$_VuCiD&O9Kj@ zSlS9p+ifwQr8N&@>}M_Q8cSNf{0>AEP$wp z3WB{LiijeLAPN@jVi_GrY+(ok_)!vQdV0+@aP zGdk)Iz%CoG#{%|L!0r%Ww+XPH0NA|;>?;7fJ%Ih_s?GwKOaNmBFj@dp3D_%~`~iDB zU{3?=4gmHX!0si0c>&m$0d~6qdpUp+0T>jpTMuAn0d@xg3>mP?1ndg|Ob%cl;j{v< z9}CzQ0GMn5qW~}(zV0Q+v#{hN% z0HzMG8=Z{+z)S})M!?=4uvY^qz`O=9FP&I`-9-Qs4`7l3OxFL_ zmiRMcOw0eLXUM1`8GjzPY5D)b4F1ypy^OY9z+c5*TZzB+zbU{OiQ=;A(d?-IIp7{M zaSUmU-d9ez3D9m$SV$3Vv*$8g6O$5cm=W4_}x$A2AfINo-==lIa^ ziQ{v}SB`HS-#dPC{Ob4vI|gfy9gm%ab;M4=;<2t+H!K-T!TMnRv4PlNEFBw$jlf1> zW3W7|0Go(S!KPy~vDsJ&HV<2XEy60XYODrZfz@Gau?<)~wiVls?Zg_ez1XQ(6BfnV zursi;uye8Vu|wG1*hAP8*nhC6uxGI6upe*}aLza~E&#{Cv2h7F5iT2-gUiK9aWb47 zSA;9ZDRK3<4qPX$3)h3|!&z|iaSL&aaZ7Q_aVv4Faf7(SxQ|naQzEB`rc_PonX+Qa z(3GvCMAKiRG}6c@G4$+zNuk%HRM3}Es>gMd%pp0Eo&21_oMN2foup2cPK{0-PDZDh zPK%rdowhmccRJ#9*6ClT`%cdQJHQF>0sH_eKm&qD3xbAGAh(Qy^O{kp{S!Dj3Y7Oo z0r0<3@cS?ddtXK&@8>Ag+2bAYQ}6&j6d#XI$4l@Eyc%DIufgl_Q}H(ZT>LWp3j8Yk z0DcXA2!9-Z0{;*G6#g{+9R33S68@z#&e_+Q;T-Lp>YVN@bQU>hJLfp(Iu|$>JL{eM zol)oCofkSUb6(-R%6ZUvuk#V-6V7LyA2`2o{_HZ&#o5Kxh3G8gi3;rP(zqXm`RvTm`j*XSV&k*SV~w!xIwr@ zxI?%{ctChWctUtactLnY_>b_G@SgCI@R{(H@SX6JIEIKJjwOyKP9#nyIudb2CnAd& zL1Ys-#28{Mkw@ecWkitJLF^=U5xa>!#6IFw;s9|CafrB{xRJP-xRtnzc!+q1c#rsi z_=xy~_>}mZ_|k2h8`X{G7VH-4#&Bc0vD_lu^4w%@akzuQ5# zVYkC>N8OIQopAfd?X24+x65wV-QJR%Nv;%lCnr*l7u8B zO(V@Ats!kE9Uz?~T_#;6T_=r@Zjx@3?vn149+DoBo{-*<-jhC(KD%Sxr?>;|&hD=6 zME6klEO)WH#9iv1?=EwfyBE1@+^gN2-640g`|s{U?pxeXyPtJG?|#w!vinu{5%(AF zuiXE0e@C83o=kQm1p!`EQML9z`OSwR~M7cuwM)^Vct9BVNb6{_;BQ_0XH*9p;_nE%8=+w|e(` z&+%UGz0rHK_g3%i-aEZ_d++f+=soOx#QT`{b?;~1uf5-Rzw`d+WA8K0XM)cpA7>wL zA73ATAF2=2C)y{;C&x$SQ|Y7k>GYZHbK2*u&v~DVK9_y2`ds%J@ww^q!WZj1#TW2( z_I33o_`3PJ`-b~Uee-<_d<%UQzQw*OU$t+qZ@;h6*W_#VwfNe6=lX8;J?DGD_mb}w z-)p|teMfw6`eFRW`i=LS=r`HV(GTb6{Br&B{A7NhUx#0(UzcCE zU$0-kpV@Dv-(P+g{Vw}m^}Fsj;&;>Uw%=X9w|+nTC-}Si6a3x$-TgiMgZx?k(f+ai zJb%7_ihr7ahJU7imVb$VrGKaYZ2vj_^Zi%&uk}CZf71Vw|A_y~09=4m06xGafDk|o zAO(;EA_MXR3IYlP6amVBl7P~H+JF@Ss{#fB)&vX%tPj{2usL8$z_x&W0S5zy0}cmV z4Y(dK5^yu%cEH_$FH{26jp|PIpi-#bR63POO{2C@d#GmW9O^3S0Cf#@h`OG-k-C|> zmAaj}lX``EjruS32K5&84)q@OIrTmD8}$eESD;;>ePCo@Twq$DB(Nw@8(0xo6<8fu z8(1IM7^n|y32X~&59|!=3Pb|UftEm9;K9JZ0xt#L4tyH;E$~O+FPa_Ao;Hp)fi{We zK*Q0TXm}cpmQPdDbhKs~Of%4WXniyj%}le&7#exEu?Lx?WP^3U83Eh-KRaI zJ*GXSJr5cagbQ*D3JIbIF@sn^5kc`mi9yLhsX^i(Nl;OcCa61TcF>Zb9Pg1?8@h2TP*LtI0MAs!)=5bqG*5Jm_$Bq1a{ zL>M9sk%u&dz#&M;Zy}3A)`n~jITmsuJU@~GD zTt*y2z(`;uF;W;phLj;^R4}?3y^MZ_kui;dGOUa_j6ud)#yZAE#%9JJjOUD(jQ<$# z7#|s57~dJcm>A|b=0v6g6UPLYE=(fRo$1N+X8JLy%phhclgSKcMl$o61d5(F3d5L+Id7U}Jd>l3=%qPq*EFg>)77`XZ zYA$Dm$-?AeMPbEZs<6^9ZCFK^A#73DlCWiAE5lZW4TP-?`#bDp*y*sdVHd(Khg}W3 z8}^mu#qweKu>x3utY8+M#bo8Ma#?w-0#*@A!BVo+tajEM);!h%)?(HY)-u*g))Ce* z)?ci@S*KWMS?5{TSZ~4!;cnsX;U3|XaG!AhaB6s3ct&_;cy_oXJU2Wqydbk7JK!)UX}cI5xm`X2-K-Y&pA#tz@g&rR-{U9lMd;%x+7o`!Es0tlwJK^bYA9+$)TXE{QQM<-Mjed$GwNv6@u+uEAEUlReUJLZ!EnZL zCU7Qk967!m4u{7Pa1uGmoI*}9N5xTdG@M3`p3}ksIX#>{j)gOw^E+o2X9s6DXAfr| z=OAa8bC`3KbDndBbB*&a=Qiga=OO1Y=RM~O=NsooG$wjn^u*}N(Zpz6TL6`VD#1K_t77tKSzIy{vQ2vw2vPtCN_o_!;guN zNs390Nskf7)W+1uG{!W?w8XTn|^EuWjmLAKDWyMCuvST^1v9XF+Wo$`oX{EcZc_b_bUz)H#TlU+@v@{oLd|_juRIX$BT=L6T~IPY2wP`D&urt1%qPQh-%i>nX4a5z`t&LkBcR22L+}*hQagX92$32aE!N>Ba@BzLvpTH;a$^2k` zBEOQaQAO9eKn17u=!oS78 z!@tje#DBtnC9oHa7fcj52yg-?0Z9-pU<;xJTmfH@AV?OZ350?yL5@HwkO|}hn_#(M zqhO0*yI_}Kk6^#xkl?W3nBauqq~NsRj^L@_z2KAJtKdicn0Wj6aq*0JR(xbUCq6bl zE7cYw+ia!#6F8)RQw}c4^)P#ftO+sS=oM1_qm#{ctS;ESMfrNDl zn-jJqY)jagus7jA!f?WogyRW+C!9(+lW;ZRd%~|oOyao2iHVaFrzGMNnTgy)equsm za$;JdFfl8!EwLjJPV7$XOEe}5>eJNcX%o{N(y(b$ z((q|6X#r`pw2(AL8Z(WbR+Cnr)|A$k){zFKb*1&B^`}|WhSD~qZBE;owj*tK+Mcv~ zY46h~q~p_l(u33K>0#+H>AZA7x*}bl-k*-7o71i7w)DN}2hxYrkEQ>Uemeb3`uX%L z>DSY5q~A;bm4V3^moXt@at1cTDTAI7mJyy2nZe13$q;5_W#nW?Gx9P@GOQWDWz5W& zm$5iwS;q2=RT*nD)@N+W*p+cH<4VT$jFF668Fw=t2cs0BPthFimF9*q6SfmXpU&UXpv~CXoYB{Xh5_^v`%zbbYJvH z^i=dh^jh?v=&k5O79q2C~*> zt%@eJ`+@ec8B@m}!(@j>yh_=xz1_>1_v_*V`l z$3ACV&cvLcoRA!P4l^e_hn>U8$;{E@Ov^!YtU1$jX5`GunVT~|XJ^hIIs0-B<{Zj7 zoO3kiubkVGF%pbqtYo}olEhJhlQ>Igk_1VTBvq0w$&_SEawIZIrKDO?D`}84NgzqL zq)%d$m?WzuYb5I=8zq}0TO`{h|45!oUP@j|-bmg{K1seve&#ZB!*kiW(Yf4Qer`f; za&B5~Ms9I#d2VfPL#{rzH5bfXp1Ue{Fn1_-L+<9>ZMi#hcjpf09?3nPdqPTmYTx<$HOx>I^fdRO{D z`c(Q#`bPRz`a$|x`XkRh&oj?E&o?h1kCw;IDe_c#>b%~(sd=V6Yu;~p zGxO%;Ez0{NZ(rWQyy3hfdB^j<<^9MXlW(6tE`MS^Hh)S!kWa`D%n!|Hg~-xm zYFUS@Q)ZC$$ogf7Y?^F_Y(TbFwobNDwnes0woi6Lc3XB&_E7dj_FVQ#_D1$z_DS|t z_M>1-fqlXFg3&YMg3tnHL3lxA0jD6kAhsZ`KvvLM&{fb=&|hFIm{wpeuoTQLSWs}M z;Bmnhxt-iq?jiS+N6C}qGPzQ&lefrwBA&@^kW=@)z>g^0)F2@=x-w@*jm`3a1p}3tbD{3dx0(LZ3qaLTVwqFtM<> zu%)oSa9-i!!X<_43U?RoEj&;-TzI|-E7GsM2imAmx#i7N_;;`cIVs^2tSYBLITwGjITvn_t?kHYZyrg(}@v7p% z;x)xX#T$x`6+bF|TKuB;b@AKccf}uyKPx?zUP@nOfHF`StPD}ol`Tq8*{D?Nsf4N;RlTZ7)uIAb?J7vsrCO?5u3D*D zty-g6r`n*}s=A=Mth%NeQQcDAR^3xQRQ)OumPks9OX^GDlHW^Kl?;|_F4CoG3Y2a=PS9$?cN6CHG5Sl)O@t)j{ewb&5JmU7#*gH>mY$Pz|fQ)qU!z zYLgmO+tf4EOVmT^4eHJ6ZR$VOXVmA_7uA>5kJazhpVVK}-%4?%zNI0hVWlyp$)%~K z;!;^@VQFz`Noi>*T52nuQ97%1PU-y8MWstiSCpg6=}uVTy2TAY_zFXm9|&AQoC9^s9mdFuid2GqCKy@q`j*BS9?=?OM6%Q zK>M>iqg+%jF3&B`D=#RQmn+Iuw@yd<;%)fmai^fQ@*ZzWBHcy1Lecz zN6L?vpDjOM{%`r+@(1OQ%b%6MsBo=ts~}fUD!eQFDyS7f6`>XR74iy2g{nebp&1Q@ z*HzS3OsyEGSW_`nvA$wc#ny`L6^ASCS3IhCTJfUdb;W-b?)ORKBbH zSox*$d*#onz^dRXdR16econ-Ux{6!Huga`ysRFB@s;;W;s=g{?)wC*0)q<+6RTru* zRb8pNRy9&}tLjeGE1iQ5rvr2@I#(T0=dO#;<>_QPxvog3)Two4x*FXyokcfY_q%SU zZnkcoZj0`r?y>Hf?uG8P?yc^O<9stB+P6uRdRWulhmt)eLT-#mSS8J>_)uOew+NHHCYFE_`)(+L~uRT$Fy>_Jb zR_&eId$sRsKh}P!{Z{*bkNzZ5>=^ ztD8|bt8Px+yt;w9U3Giv_SYS(8?HNBcc<=t-J`lEbWdX zhBXaG8!k6oYZz&`*>JnzUE|os@r{!j9U8HXZjC{WA&rd2u*UF4L1Rv1?r4mSyiw6u z-PqY^XzXq5Z!|W}Zyaje(73sAYvcCDzZ$PKjx^qCywiBE@nh43rpZm%rYTK86QzmS z#A=FcifW2!5;lpN#7&Z>yr%M|s-~Kzx~9ga)+R$!Z`0H!q{-5>xM^9_%BIy#gH3yz zPBxusI^T4$>2lNarteL^^cekE{dm2Ho~{qmN9fsljy_AT(wFMB`U-uOzC~};oAjvO zrk|l-rQf07rQf69uRo+er@x@TtiPro(ZACFYPM@0+dQFpax=M^((Kdh*BsEyYtCxU zX_hwUHy1RQHtU;Pn!#qMxvSaOJhyp4^Wx^E&C8p&H6Lj{-u!p-$>!6|_nY50e`@~P z{Jr^Si*t*A3$-PvC8UMk!fg??WVc9Kq%HX^x)!LVtEH!;+BmqTUxfa>}=WH za<=78%l(!|El*mWwR~)y&^oyl+d8EcX!UGmw6awykYj-?pi3Yuk>t-EDi?_O~5u`?Kw6+h1+}w4H7{*LI=pQrp$G zf7@=h-D$hm_OR_q+w-K9QK29x1foM>Pz1z=qM=wQ4vL2op=2lx5<((K4CO+3Pytj3 zDIg`JhRPrUz!v=CYhErnJ< ztDpgBEwmom2yKD3K|7&8pncE*=n!-mItKj({R5qX&O#TUOVAbQIy3^^g6={OpvTZt z=mqo|dJDaWK0#lgZ_v-qF`bysv7Hk-CwDq_PU&>(eA@Y<^L6Lj&JUfRJHK`QgvY=b zcpN+tc7SoP6O4ylVKrY=x)8zr(ZPdGG>w5xf*$0k4MF zz(eo`coV!8-VX1A|A6cVt6blvE>-F2_)Vb_zc=UuP5-gLe1`qcHc>xW^C!QL?5Fv;L( zm}0;iTn%mpvVmgoG58w-4Z#MwAbc)Ds>1Ij^al0vdb4|rdmDSZduR8q?A_9PsP|&;quvj_ zKl<=}0ezglj6Qi^ZJ(jf*0-#0Ti>637yBOded%}Xr}WeMIsHlfqJC9>V}D0~Pk(3jWT1QQDIaX)y6Voxv|n%ZLBjk81=?h zW4p1_XfXB~ry5O0)Mzu#FwQd0HO@CKGA=bPH?A@c8rK@v8#fuZ8h03X8}}Ly7>A8V zjK_^9jQ4*>!Az~yK$wLZ|B1DO( z5e-s-=#W~Z0nsC^2#9nbFw%|mAyW|(f+99#1~Lnoi!4AEAxn`J$SPzI8A3K7n~|-^ z4rDj77uk;-M23+g$T8#uauPX%oJTGpSCM~_8^~?s9`X=*f;>lFA#afP$S340@&oy0 z!kEUHCYl^fSd){<*+ejrOk@+qzu(-R8aK1Lk4#5%Y2L-{w>1v*rut%jRq55%Vqc zUGoF;WAiifOY?u`cjk}gFXr#&UnmA0hfYKtP#g-NE+`RoM?Fz*)DNYiL1-w-M8i=w z8jW&MKAM0gqiLuR%|dffDJnw?(PFd&Ekn!EDzpZzN1MO zAEHmt=jbc+4f-DagnmVTSjJfFE#obdERL2b7QDsP;$|URC>9@!za`KTY@u7iED@F{ zON@nQ5m*u}DVB6grX|}VvE*3_EJYTjMQzboDl9rnt);=Dx3pT?Eu9vFrPngmVzQtX zn`MS&mSwJGfn~8}nPsJAz_Qk|-m=NE)w08~+p^bkz%pz(VmWU4+j7cs)^fpe*>cS? zV!36xYk6RKYS85Y-L0NhZ>z60z)G`* zSQ%E9HPXtl##-a7@zx}3sx`wZvWl&_)_kkns<5i8rBdn~wbN>_ z_FAV}O;*%uv(B*2vd*3IaZFaV?wh6Y$HmuFb=4>O_NHz}} z#pYx4w*}gQZFF0hEy5OMi?Q)+0$ZXj#g=Z%v}M~Qwme&bt;nXdscjltg-vIxwKdrE zwpJTxgKS;49$UW+v6*dF+i$j+wmG(WwuQDOw&k`}wn5vFZG&yIZJTYU?GM{N+dHmL}GG;W(>3=T#_5bhrfA{HA{vQe) BY@+}G diff --git a/macosx/English.lproj/MainMenu.nib/classes.nib b/macosx/English.lproj/MainMenu.nib/classes.nib index ddefb88a2..d6ecb7e50 100644 --- a/macosx/English.lproj/MainMenu.nib/classes.nib +++ b/macosx/English.lproj/MainMenu.nib/classes.nib @@ -7,6 +7,7 @@ applyFilter = id; applySpeedLimit = id; copyTorrentFiles = id; + createFile = id; doNothing = id; linkForums = id; linkHomepage = id; @@ -57,7 +58,7 @@ fDownloadLimitItem = NSMenuItem; fDownloadMenu = NSMenu; fDownloadNoLimitItem = NSMenuItem; - fFilterBar = ImageBackgroundView; + fFilterBar = FilterBarView; fNameSortActionItem = NSMenuItem; fNameSortItem = NSMenuItem; fNextFilterItem = NSMenuItem; diff --git a/macosx/English.lproj/MainMenu.nib/info.nib b/macosx/English.lproj/MainMenu.nib/info.nib index d5fa456e5..1139f63eb 100644 --- a/macosx/English.lproj/MainMenu.nib/info.nib +++ b/macosx/English.lproj/MainMenu.nib/info.nib @@ -32,7 +32,7 @@ IBOpenObjects 21 - 1603 + 29 IBSystem Version 8P135 diff --git a/macosx/English.lproj/MainMenu.nib/keyedobjects.nib b/macosx/English.lproj/MainMenu.nib/keyedobjects.nib index 4a7c03fed179cf5e1fe14560b8e0fa1e24733b5a..ad377a4d7a0126b3f794afd1d9b4afa16af5d475 100644 GIT binary patch literal 54460 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 literal 54228 zcmcG%2YggT_cuP}?(V&Nw`C!B0TCjiC<-K@H-XTk6B2qeBpV1M*^ojJ(K|L!K>GUi*kCxH-bOEiRi}7s&?cparm3g=CEQ`$`-InwumieOV~0d z*rn_Wb`@LCHn2@>GrNIpW82y7>`rzM`wx47z0P*CJ!~&~i@nc2VZX9J0QVnGxt|Ak zkT>J4@p(LN!%yI+^X|L{@5y`d-nYS8;RpG*tSkSH|Hyyh zzwlrAA-0?U#*gqn_)*QRc{E)!w1AeWwbWW^$7xyGDOxA3v(`oHrghi)XnpAy+CXin zHd@QqCTIoP6m7P4mR6*dY89&8LTwSC=-P7aeC-nLQtdKrm9|z}r`@1!*KXDB*6z_B z)E?5F(ROJsYkRdfwD+|Sv`@58wQsdwv|qJDX#0nYxI8Z1Ww-*aOjk=+E7x(Z)~+m9 z2iGaCQ(c{0-CW&WJzRZVd9J~(A+DjWk*<7gwriqxmTQu0x|Z*nqfKxXyXLuKt{PXZ ztIl<{OW^Z-*9zC=>drdXHR{f0*EZMft~*>iT>o)BgwKavkGr13=kuBQhU;C| zetaHqeeU`epWnHDb{)azAFiWrx9bkK$L(`B!)J4MOLrDNPjH_s;ki4xb0s`?Z}%Az zo_nZ!q=e_rcTbY=+%w&CBs_PSyHdh)*SXJD@Pzwf1#h)`or1T~z1e-U`!@IO?mOJ~ zxF5vlL+;1i&*Jkr_b&GyeC~C>S&*3;f|lBc8R zG*7lC$J4{p)6>t>-;?JV;lAB7(lgF837?ZaGdv}p`JM%y8c(ffspo9Zxt{Yp7kXBC zR(sZauJ&Bxx!$wMv(`OS0K^SkGW z=MSCeRM&Jv_v(H!i2QI_quq6SZITj-Gz{X?nJ{OYf=o(tGP?XnXN( zs6I>|rM<6D&`qTO|xOx$vuj+fXPxSZn_wl!1|4cuq|DYey z|2DW`8eyZEajMbL$T4z_u0}tjzcI)dX^b*P^A^S!Bj1={Of;q#MSP_($0#-y80AK- zQD-b=LE~KGeB%ORg>i|o%D6(c*kEilZZK|C-)}eWFzz+(Q{Q(Qj~UMz&l%5qjufI~adw;%^N8Cg5+fcZPSScaFE%JI`C@t@Osc zwca}XosGZq@OPnirS}T&8t+=~I`1{!8|2*g-sru>yWM+-_g?RV-iN%8dY|wcE??La+`25BDJKOC&Y7�ZqsA>Ouw07wlG_ot<1LO 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 diff --git a/macosx/FileBrowserCell.h b/macosx/FileBrowserCell.h index b716d30a3..9446ed397 100644 --- a/macosx/FileBrowserCell.h +++ b/macosx/FileBrowserCell.h @@ -26,6 +26,9 @@ @interface FileBrowserCell : NSBrowserCell { + float fPercent; } +- (void) setProgress: (float) progress; + @end diff --git a/macosx/FileBrowserCell.m b/macosx/FileBrowserCell.m index 2b86bba0c..036a01c9c 100644 --- a/macosx/FileBrowserCell.m +++ b/macosx/FileBrowserCell.m @@ -36,11 +36,19 @@ - (void) setImage: (NSImage *) image { + if (!image) + image = [[[[NSWorkspace sharedWorkspace] iconForFileType: NSFileTypeForHFSTypeCode('fldr')] copy] autorelease]; + [image setFlipped: YES]; [image setScalesWhenResized: YES]; [super setImage: image]; } +- (void) setProgress: (float) progress +{ + fPercent = progress * 100.0; +} + - (void) drawWithFrame: (NSRect) cellFrame inView: (NSView *) controlView { NSMutableDictionary * item; @@ -85,8 +93,7 @@ paragraphStyle, NSParagraphStyleAttributeName, nil]; NSString * statusString = [NSString stringWithFormat: NSLocalizedString(@"%.2f%% of %@", - "Inspector -> Files tab -> file status string"), - 100.0 * [[item objectForKey: @"Progress"] floatValue], + "Inspector -> Files tab -> file status string"), fPercent, [NSString stringForFileSize: [[item objectForKey: @"Size"] unsignedLongLongValue]]]; NSRect statusTextRect = nameTextRect; diff --git a/macosx/FileOutlineView.h b/macosx/FileOutlineView.h index 7a416ecd3..8909b4819 100644 --- a/macosx/FileOutlineView.h +++ b/macosx/FileOutlineView.h @@ -26,6 +26,7 @@ @interface FileOutlineView : NSOutlineView { + NSColor * fNormalColor, * fHighPriorityColor, * fLowPriorityColor; } @end diff --git a/macosx/FileOutlineView.m b/macosx/FileOutlineView.m index 56c758519..01ceef74a 100644 --- a/macosx/FileOutlineView.m +++ b/macosx/FileOutlineView.m @@ -24,6 +24,7 @@ #import "FileOutlineView.h" #import "FileBrowserCell.h" +#import "Torrent.h" @implementation FileOutlineView @@ -34,6 +35,18 @@ [self setAutoresizesOutlineColumn: NO]; [self setIndentationPerLevel: 14.0]; + + fNormalColor = [self backgroundColor]; + fHighPriorityColor = [[NSColor colorWithCalibratedRed: 1.0 green: 208.0/255.0 blue: 208.0/255.0 alpha: 1.0] retain]; + fLowPriorityColor = [[NSColor colorWithCalibratedRed: 1.0 green: 1.0 blue: 224.0/255.0 alpha: 1.0] retain]; +} + +- (void) dealloc +{ + [fHighPriorityColor release]; + [fLowPriorityColor release]; + + [super dealloc]; } - (void) mouseDown: (NSEvent *) event @@ -42,13 +55,13 @@ [super mouseDown: event]; } -- (NSMenu *) menuForEvent: (NSEvent *) e +- (NSMenu *) menuForEvent: (NSEvent *) event { - int row = [self rowAtPoint: [self convertPoint: [e locationInWindow] fromView: nil]]; + int row = [self rowAtPoint: [self convertPoint: [event locationInWindow] fromView: nil]]; if (row >= 0) { - if ([self itemAtRow: row] && ![self isRowSelected: row]) + if (![self isRowSelected: row]) [self selectRowIndexes: [NSIndexSet indexSetWithIndex: row] byExtendingSelection: NO]; } else @@ -57,4 +70,65 @@ return [self menu]; } +- (void) drawRow: (int) row clipRect: (NSRect) clipRect +{ + if (![self isRowSelected: row]) + { + NSDictionary * item = [self itemAtRow: row]; + if ([[item objectForKey: @"IsFolder"] boolValue]) + [fNormalColor set]; + else + { + int priority = [[item objectForKey: @"Priority"] intValue]; + if (priority == PRIORITY_HIGH) + [fHighPriorityColor set]; + else if (priority == PRIORITY_LOW) + [fLowPriorityColor set]; + else + [fNormalColor set]; + } + + NSRect rect = [self rectOfRow: row]; + rect.size.height -= 1.0; + + NSRectFill(rect); + } + + [super drawRow: row clipRect: clipRect]; +} + +- (void) drawRect: (NSRect) r +{ + [super drawRect: r]; + + NSDictionary * item; + int i, priority; + for (i = 0; i < [self numberOfRows]; i++) + { + if ([self isRowSelected: i]) + { + item = [self itemAtRow: i]; + if (![[item objectForKey: @"IsFolder"] boolValue]) + { + priority = [[item objectForKey: @"Priority"] intValue]; + if (priority == PRIORITY_HIGH) + [fHighPriorityColor set]; + else if (priority == PRIORITY_LOW) + [fLowPriorityColor set]; + else + continue; + + NSRect rect = [self rectOfRow: i]; + float width = 14.0; + rect.origin.y += (rect.size.height - width) * 0.5; + rect.origin.x += 3.0; + rect.size.width = width; + rect.size.height = width; + + [[NSBezierPath bezierPathWithOvalInRect: rect] fill]; + } + } + } +} + @end diff --git a/macosx/IPCController.m b/macosx/IPCController.m index af6c8a2ed..8028a33eb 100644 --- a/macosx/IPCController.m +++ b/macosx/IPCController.m @@ -470,8 +470,7 @@ msg_default ( enum ipc_msg msgid, benc_val_t * val, int64_t tag, void * arg ); if( IPC_MSG_INFO == respid ) res = ipc_addinfo( pkinf, [tor torrentID], [tor torrentInfo], types ); else - res = ipc_addstat( pkinf, [tor torrentID], [tor torrentInfo], - [tor torrentStat], types ); + res = ipc_addstat( pkinf, [tor torrentID], [tor torrentStat], types ); if( 0 > res ) { tr_bencFree( &packet ); diff --git a/macosx/Images/Create.png b/macosx/Images/Create.png new file mode 100644 index 0000000000000000000000000000000000000000..3aaac1b0beee803cc5199f567d3fb9bf320315bb GIT binary patch literal 1256 zcmVP)(_`g8%^e{{R4h=>PzAFaQARU;qF*m;eA5Z<1fd zMgRZ=Zb?KzRCwB~m(On-MHI(BZ@jiQ-l|GNP1>S>3I}qigwz8fsgw(UKn_qOR2B75 zlvBC%1P6{iRZ$O>P%f2FWtE-^;?`bJ+Dk=cZaEQ4AdsMOe*>Lv-7_1`@ZkZj5z1WwyCM9nM;>0eO0Mcj#z6g&fx&(+(@_9qO~RrL$TKO>h=1) zjg5^u2ehnB$pe^~nfahzuRp3*t4Flf;2d^fEZdDS7-P^{)9G|*wOVX#ZFN>xS1&$% z`0(DO0K73Tr`qkd@_iqzb^1X}hD(j-p9g?~t5>hy7Lg0<>+3%;K~s}V5)qVAX)hEC zL(j!xkst^V5yCJGZr;3ETU=aReq{h6GSU-cN%uT&Xk03ls8lL+Ivq}*J{_#Bt<@G5 z7M5QdfIJWp839x-m*Yj1;>?*d!L3`jYUj?KTRt>^EZs_}5dhgR z9Nf5ZqqelPv?mDfAO?~#fQuD5^xh4|)>_KtGM?vg=gu8=c6KP0N}N1-GPr#Ca;@L* zfByLK&fYXQ(&)9dw6O1(8VH@7emfF!obBC)p& zc|Kb?!PiUQAi%ZtYj~cQ3Nr|Tp`z9A_lcqiAecx4S>X+7U|<};Qy%lx_rZZDUiytv zYPV8agEbbV6u$3MC=_I35hfZK_>FUCkC;Nd@ikHz#Kt|@~ z*KyA6hT~O2kS-LT;XnjW3d|jSJIR}&{Lh`S5K&`mz+OkP5g;Qp04_AR?Rc(@+}LUv z{~;jk$gOhT(|A z`300xZ%$87e 0 && [[[fTabView selectedTabViewItem] identifier] isEqualToString: TAB_FILES_IDENT]; + + if (action == @selector(setCheck:)) + { + Torrent * torrent = [fTorrents objectAtIndex: 0]; + NSDictionary * item; + NSIndexSet * indexSet = [fFileOutline selectedRowIndexes], * itemIndexes; + NSMutableIndexSet * usedIndexes = [NSMutableIndexSet indexSet]; + int i, index, state = (menuItem == fFileCheckItem) ? NSOnState : NSOffState; + for (i = [indexSet firstIndex]; i != NSNotFound; i = [indexSet indexGreaterThanIndex: i]) + { + itemIndexes = [[fFileOutline itemAtRow: i] objectForKey: @"Indexes"]; + if (![usedIndexes containsIndexes: itemIndexes]) + { + if ([torrent checkForFiles: itemIndexes] != state && [torrent canChangeDownloadCheckForFiles: itemIndexes]) + return YES; + [usedIndexes addIndexes: itemIndexes]; + } + } + return NO; + } + + if (action == @selector(setPriority:)) + { + if ([fFileOutline numberOfSelectedRows] <= 0) + { + [menuItem setState: NSOffState]; + return NO; + } + //determine which priorities are checked + NSIndexSet * indexSet = [fFileOutline selectedRowIndexes]; + BOOL current = NO, other = NO; + int i, priority; + Torrent * torrent = [fTorrents objectAtIndex: 0]; + + if (menuItem == fFilePriorityHigh) + priority = PRIORITY_HIGH; + else if (menuItem == fFilePriorityLow) + priority = PRIORITY_LOW; + else + priority = PRIORITY_NORMAL; + + for (i = [indexSet firstIndex]; i != NSNotFound && (!current || !other); i = [indexSet indexGreaterThanIndex: i]) + { + if ([torrent hasFilePriority: priority forIndexes: [[fFileOutline itemAtRow: i] objectForKey: @"Indexes"]]) + current = YES; + else + other = YES; + } + + [menuItem setState: current ? (other ? NSMixedState : NSOnState) : NSOffState]; + return YES; + } + return YES; } @@ -771,31 +828,24 @@ - (int) outlineView: (NSOutlineView *) outlineView numberOfChildrenOfItem: (id) item { if (!item) - return fFiles ? [fFiles count] : 1; + return [fFiles count]; return [[item objectForKey: @"IsFolder"] boolValue] ? [[item objectForKey: @"Children"] count] : 0; } - (BOOL) outlineView: (NSOutlineView *) outlineView isItemExpandable: (id) item { - return item && [[item objectForKey: @"IsFolder"] boolValue]; + return [[item objectForKey: @"IsFolder"] boolValue]; } - (id) outlineView: (NSOutlineView *) outlineView child: (int) index ofItem: (id) item { - if (!fFiles) - return nil; - return [(item ? [item objectForKey: @"Children"] : fFiles) objectAtIndex: index]; } -- (id) outlineView: (NSOutlineView *) outlineView objectValueForTableColumn: (NSTableColumn *) tableColumn - byItem: (id) item +- (id) outlineView: (NSOutlineView *) outlineView objectValueForTableColumn: (NSTableColumn *) tableColumn byItem: (id) item { - if (!item) - return nil; - if ([[tableColumn identifier] isEqualToString: @"Check"]) - return [item objectForKey: @"Check"]; + return [NSNumber numberWithInt: [[fTorrents objectAtIndex: 0] checkForFiles: [item objectForKey: @"Indexes"]]]; else return item; } @@ -803,26 +853,27 @@ - (void) outlineView: (NSOutlineView *) outlineView willDisplayCell: (id) cell forTableColumn: (NSTableColumn *) tableColumn item: (id) item { - if ([[tableColumn identifier] isEqualToString: @"Name"]) + NSString * identifier = [tableColumn identifier]; + if ([identifier isEqualToString: @"Name"]) { - if (!item) - return; - - [cell setImage: [[item objectForKey: @"IsFolder"] boolValue] ? fFolderIcon : [item objectForKey: @"Icon"]]; - } - else if ([[tableColumn identifier] isEqualToString: @"Check"]) - { - /*if (!item) + if ([[item objectForKey: @"IsFolder"] boolValue]) + [cell setImage: nil]; + else { - [(NSButtonCell *)cell setImagePosition: NSNoImage]; - [cell setEnabled: NO]; - return; + [cell setImage: [item objectForKey: @"Icon"]]; + [(FileBrowserCell *)cell setProgress: [[fTorrents objectAtIndex: 0] fileProgress: + [[item objectForKey: @"Indexes"] firstIndex]]]; } - - [(NSButtonCell *)cell setImagePosition: NSImageOnly]; - [cell setEnabled: [[item objectForKey: @"IsFolder"] boolValue] ? [[item objectForKey: @"Remaining"] intValue] > 0 - : [[item objectForKey: @"Progress"] floatValue] < 1.0];*/ - [(NSButtonCell *)cell setImagePosition: NSNoImage]; + } + else if ([identifier isEqualToString: @"Check"]) + [cell setEnabled: [[fTorrents objectAtIndex: 0] canChangeDownloadCheckForFiles: [item objectForKey: @"Indexes"]]]; + else if ([identifier isEqualToString: @"Priority"]) + { + Torrent * torrent = [fTorrents objectAtIndex: 0]; + NSIndexSet * indexeSet = [item objectForKey: @"Indexes"]; + [(NSSegmentedCell *)cell setSelected: [torrent hasFilePriority: PRIORITY_LOW forIndexes: indexeSet] forSegment: 0]; + [(NSSegmentedCell *)cell setSelected: [torrent hasFilePriority: PRIORITY_NORMAL forIndexes: indexeSet] forSegment: 1]; + [(NSSegmentedCell *)cell setSelected: [torrent hasFilePriority: PRIORITY_HIGH forIndexes: indexeSet] forSegment: 2]; } else; } @@ -830,24 +881,63 @@ - (void) outlineView: (NSOutlineView *) outlineView setObjectValue: (id) object forTableColumn: (NSTableColumn *) tableColumn byItem: (id) item { - Torrent * torrent = [fTorrents objectAtIndex: 0]; - int state = [object intValue] != NSOffState ? NSOnState : NSOffState; - - [torrent setFileCheckState: state forFileItem: item]; - NSMutableDictionary * topItem = [torrent resetFileCheckStateForItemParent: item]; - - [fFileOutline reloadItem: topItem reloadChildren: YES]; + NSString * identifier = [tableColumn identifier]; + if ([identifier isEqualToString: @"Check"]) + { + Torrent * torrent = [fTorrents objectAtIndex: 0]; + [torrent setFileCheckState: [object intValue] != NSOffState ? NSOnState : NSOffState + forIndexes: [item objectForKey: @"Indexes"]]; + [fFileOutline reloadData]; + } + else if ([identifier isEqualToString: @"Priority"]) + { + int priority = [object intValue], actualPriority; + if (priority == 0) + actualPriority = PRIORITY_LOW; + else if (priority == 2) + actualPriority = PRIORITY_HIGH; + else + actualPriority = PRIORITY_NORMAL; + + [[fTorrents objectAtIndex: 0] setFilePriority: actualPriority forIndexes: [item objectForKey: @"Indexes"]]; + [fFileOutline reloadData]; + } + else; } - (NSString *) outlineView: (NSOutlineView *) outlineView toolTipForCell: (NSCell *) cell rect: (NSRectPointer) rect tableColumn: (NSTableColumn *) tableColumn item: (id) item mouseLocation: (NSPoint) mouseLocation { - if (!item) - return nil; - NSString * ident = [tableColumn identifier]; if ([ident isEqualToString: @"Name"]) return [[[fTorrents objectAtIndex: 0] downloadFolder] stringByAppendingPathComponent: [item objectForKey: @"Path"]]; + else if ([ident isEqualToString: @"Check"]) + { + int check = [cell state]; + if (check == NSOffState) + return NSLocalizedString(@"Don't Download", "Inspector -> files tab -> tooltip"); + else if (check == NSMixedState) + return NSLocalizedString(@"Download Some", "Inspector -> files tab -> tooltip"); + else + return NSLocalizedString(@"Download", "Inspector -> files tab -> tooltip"); + } + else if ([ident isEqualToString: @"Priority"]) + { + Torrent * torrent = [fTorrents objectAtIndex: 0]; + NSIndexSet * indexSet = [item objectForKey: @"Indexes"]; + BOOL low = [torrent hasFilePriority: PRIORITY_LOW forIndexes: indexSet], + normal = [torrent hasFilePriority: PRIORITY_NORMAL forIndexes: indexSet], + high = [torrent hasFilePriority: PRIORITY_HIGH forIndexes: indexSet]; + + if (low && !normal && !high) + return NSLocalizedString(@"Low Priority", "Inspector -> files tab -> tooltip"); + else if (!low && normal && !high) + return NSLocalizedString(@"Normal Priority", "Inspector -> files tab -> tooltip"); + else if (!low && !normal && high) + return NSLocalizedString(@"High Priority", "Inspector -> files tab -> tooltip"); + else + return NSLocalizedString(@"Multiple Priorities", "Inspector -> files tab -> tooltip"); + } else return nil; } @@ -860,11 +950,6 @@ return [outlineView rowHeight]; } -- (BOOL) outlineView: (NSOutlineView *) outlineView shouldSelectItem: (id) item -{ - return item != nil; -} - - (NSArray *) peerSortDescriptors { NSMutableArray * descriptors = [NSMutableArray array]; @@ -917,6 +1002,46 @@ [[fFileOutline itemAtRow: i] objectForKey: @"Path"]] inFileViewerRootedAtPath: nil]; } +- (void) setCheck: (id) sender +{ + int state = sender == fFileCheckItem ? NSOnState : NSOffState; + + Torrent * torrent = [fTorrents objectAtIndex: 0]; + NSIndexSet * indexSet = [fFileOutline selectedRowIndexes], * itemIndexes; + NSMutableIndexSet * usedIndexes = [NSMutableIndexSet indexSet]; + int i; + for (i = [indexSet firstIndex]; i != NSNotFound; i = [indexSet indexGreaterThanIndex: i]) + { + itemIndexes = [[fFileOutline itemAtRow: i] objectForKey: @"Indexes"]; + if (![usedIndexes containsIndexes: itemIndexes]) + { + [torrent setFileCheckState: state forIndexes: itemIndexes]; + [usedIndexes addIndexes: itemIndexes]; + } + } + + [fFileOutline reloadData]; +} + +- (void) setPriority: (id) sender +{ + int priority; + if (sender == fFilePriorityHigh) + priority = PRIORITY_HIGH; + else if (sender == fFilePriorityLow) + priority = PRIORITY_LOW; + else + priority = PRIORITY_NORMAL; + + Torrent * torrent = [fTorrents objectAtIndex: 0]; + NSIndexSet * indexSet = [fFileOutline selectedRowIndexes]; + int i; + for (i = [indexSet firstIndex]; i != NSNotFound; i = [indexSet indexGreaterThanIndex: i]) + [torrent setFilePriority: priority forIndexes: [[fFileOutline itemAtRow: i] objectForKey: @"Indexes"]]; + + [fFileOutline reloadData]; +} + - (void) setLimitSetting: (id) sender { BOOL upload = sender == fUploadLimitPopUp; diff --git a/macosx/PiecesView.h b/macosx/PiecesView.h index 9284991f8..a18f9e0c8 100644 --- a/macosx/PiecesView.h +++ b/macosx/PiecesView.h @@ -34,7 +34,7 @@ * fBlue1Piece, * fBlue2Piece, * fBlue3Piece, * fBlue4Piece; Torrent * fTorrent; - int fNumPieces, fAcross, fWidth, fExtraBorder; + int fNumPieces, fAcross, fWidth, fExtraBorder; IBOutlet NSImageView * fImageView; } diff --git a/macosx/PiecesView.m b/macosx/PiecesView.m index 9b53e92b0..c5d46a264 100644 --- a/macosx/PiecesView.m +++ b/macosx/PiecesView.m @@ -43,6 +43,8 @@ - (void) awakeFromNib { + #warning NSRectFill + NSSize size = [fImageView bounds].size; NSBezierPath * bp = [NSBezierPath bezierPathWithRect: [fImageView bounds]]; diff --git a/macosx/Spanish.lproj/InfoPlist.strings b/macosx/Spanish.lproj/InfoPlist.strings index 2673ac582..ea811ccac 100644 --- a/macosx/Spanish.lproj/InfoPlist.strings +++ b/macosx/Spanish.lproj/InfoPlist.strings @@ -1,4 +1,4 @@ /* Localized versions of Info.plist keys */ CFBundleName = "Transmission"; -NSHumanReadableCopyright = "Copyright 2005-2007 Eric Petit"; \ No newline at end of file +NSHumanReadableCopyright = "Copyright 2005-2007 The Transmission Project"; \ No newline at end of file diff --git a/macosx/StringAdditions.m b/macosx/StringAdditions.m index 3fa38b1e9..7cd205ec5 100644 --- a/macosx/StringAdditions.m +++ b/macosx/StringAdditions.m @@ -47,7 +47,7 @@ if (size < 1024) return [NSString stringWithFormat: NSLocalizedString(@"%lld bytes", "File size"), size]; - float convertedSize = (float) size; + float convertedSize = (float)size; NSString * unit; if (size < 1048576) { diff --git a/macosx/Torrent.h b/macosx/Torrent.h index e94e7cc49..3c8854933 100644 --- a/macosx/Torrent.h +++ b/macosx/Torrent.h @@ -25,6 +25,10 @@ #import #import +#define PRIORITY_LOW -1 +#define PRIORITY_NORMAL 0 +#define PRIORITY_HIGH 1 + #define INVALID -99 @interface Torrent : NSObject @@ -53,7 +57,7 @@ NSImage * fIcon, * fIconFlipped, * fIconSmall; NSMutableString * fNameString, * fProgressString, * fStatusString, * fShortStatusString, * fRemainingTimeString; - NSArray * fFileList, * fFileFlatList; + NSArray * fFileList, * fFlatFileList; int fUploadLimit, fDownloadLimit; float fRatioLimit; @@ -93,8 +97,6 @@ - (void) resetCache; -- (BOOL) allDownloaded; - - (float) ratio; - (int) ratioSetting; - (void) setRatioSetting: (int) setting; @@ -162,9 +164,9 @@ - (BOOL) isPaused; - (BOOL) isWaitingToChecking; - (BOOL) isChecking; +- (BOOL) allDownloaded; - (BOOL) isError; - (NSString *) errorMessage; -- (BOOL) justFinished; - (NSArray *) peers; @@ -186,12 +188,12 @@ - (int) peersUploading; - (int) peersDownloading; -- (float) downloadRate; -- (float) uploadRate; -- (uint64_t) downloadedValid; -- (uint64_t) downloadedTotal; -- (uint64_t) uploadedTotal; -- (float) swarmSpeed; +- (float) downloadRate; +- (float) uploadRate; +- (uint64_t) downloadedValid; +- (uint64_t) downloadedTotal; +- (uint64_t) uploadedTotal; +- (float) swarmSpeed; - (BOOL) pex; - (void) setPex: (BOOL) setting; @@ -201,9 +203,12 @@ - (NSArray *) fileList; - (int) fileCount; -- (BOOL) updateFileProgress; -- (void) setFileCheckState: (int) state forFileItem: (NSMutableDictionary *) item; -- (NSMutableDictionary *) resetFileCheckStateForItemParent: (NSMutableDictionary *) originalChild; +- (float) fileProgress: (int) index; +- (int) checkForFiles: (NSIndexSet *) indexSet; +- (BOOL) canChangeDownloadCheckForFiles: (NSIndexSet *) indexSet; +- (void) setFileCheckState: (int) state forIndexes: (NSIndexSet *) indexSet; +- (void) setFilePriority: (int) priority forIndexes: (NSIndexSet *) indexSet; +- (BOOL) hasFilePriority: (int) priority forIndexes: (NSIndexSet *) indexSet; - (NSDate *) dateAdded; - (NSDate *) dateCompleted; diff --git a/macosx/Torrent.m b/macosx/Torrent.m index 9e65095c3..9d57b146c 100644 --- a/macosx/Torrent.m +++ b/macosx/Torrent.m @@ -43,12 +43,14 @@ static int static_lastid = 0; checkUpload: (NSNumber *) checkUpload uploadLimit: (NSNumber *) uploadLimit checkDownload: (NSNumber *) checkDownload downloadLimit: (NSNumber *) downloadLimit pex: (NSNumber *) pex - waitToStart: (NSNumber *) waitToStart orderValue: (NSNumber *) orderValue; + waitToStart: (NSNumber *) waitToStart orderValue: (NSNumber *) orderValue + filesShouldDownload: (NSArray *) filesShouldDownload filePriorities: (NSArray *) filePriorities; +- (void) historyFilePriorities: (NSMutableArray *) history forItems: (NSArray *) items; -- (void) createFileList; +- (void) createFileListShouldDownload: (NSArray *) filesShouldDownload priorities: (NSArray *) filePriorities; - (void) insertPath: (NSMutableArray *) components forSiblings: (NSMutableArray *) siblings - withParent: (NSMutableDictionary *) parent previousPath: (NSString *) previousPath - fileSize: (uint64_t) size state: (int) state flatList: (NSMutableArray *) flatList; + withParent: (NSMutableDictionary *) parent previousPath: (NSString *) previousPath + flatList: (NSMutableArray *) flatList fileSize: (uint64_t) size index: (int) index priority: (int) priority; - (NSImage *) advancedBar; - (void) trashFile: (NSString *) path; @@ -79,7 +81,8 @@ static uint32_t kRed = BE(0xFF6450FF), //255, 100, 80 checkUpload: nil uploadLimit: nil checkDownload: nil downloadLimit: nil pex: nil - waitToStart: nil orderValue: nil]; + waitToStart: nil orderValue: nil + filesShouldDownload: nil filePriorities: nil]; if (self) { @@ -109,7 +112,9 @@ static uint32_t kRed = BE(0xFF6450FF), //255, 100, 80 downloadLimit: [history objectForKey: @"DownloadLimit"] pex: [history objectForKey: @"Pex"] waitToStart: [history objectForKey: @"WaitToStart"] - orderValue: [history objectForKey: @"OrderValue"]]; + orderValue: [history objectForKey: @"OrderValue"] + filesShouldDownload: [history objectForKey: @"FilesShouldDownload"] + filePriorities: [history objectForKey: @"FilePriorities"]]; if (self) { @@ -167,7 +172,24 @@ static uint32_t kRed = BE(0xFF6450FF), //255, 100, 80 [NSNumber numberWithInt: fCheckDownload], @"CheckDownload", [NSNumber numberWithInt: fDownloadLimit], @"DownloadLimit", [NSNumber numberWithBool: fWaitToStart], @"WaitToStart", - [self orderValue], @"OrderValue", nil]; + [self orderValue], @"OrderValue", + nil]; + + //set file should download + int fileCount = [self fileCount]; + NSMutableArray * filesShouldDownload = [NSMutableArray arrayWithCapacity: fileCount]; + + tr_priority_t * priorities = tr_torrentGetFilePriorities(fHandle); + int i; + for (i = 0; i < fileCount; i++) + [filesShouldDownload addObject: [NSNumber numberWithBool: priorities[i] != TR_PRI_DND]]; + free(priorities); + [history setObject: filesShouldDownload forKey: @"FilesShouldDownload"]; + + //set file priorities + NSMutableArray * filePriorities = [NSMutableArray arrayWithCapacity: fileCount]; + [self historyFilePriorities: filePriorities forItems: fFileList]; + [history setObject: filePriorities forKey: @"FilePriorities"]; if (fUseIncompleteFolder) [history setObject: fIncompleteFolder forKey: @"IncompleteFolder"]; @@ -223,7 +245,7 @@ static uint32_t kRed = BE(0xFF6450FF), //255, 100, 80 [fRemainingTimeString release]; [fFileList release]; - [fFileFlatList release]; + [fFlatFileList release]; [fBitmap release]; free(fPieces); @@ -284,7 +306,7 @@ static uint32_t kRed = BE(0xFF6450FF), //255, 100, 80 fStat = tr_torrentStat(fHandle); //notification when downloading finished - if ([self justFinished]) + if (tr_getComplete(fHandle) || tr_getDone(fHandle)) { BOOL canMove = YES; @@ -322,6 +344,9 @@ static uint32_t kRed = BE(0xFF6450FF), //255, 100, 80 fStat = tr_torrentStat(fHandle); [[NSNotificationCenter defaultCenter] postNotificationName: @"TorrentFinishedDownloading" object: self]; } + else if (tr_getIncomplete(fHandle)) + [[NSNotificationCenter defaultCenter] postNotificationName: @"UpdateQueue" object: self]; + else; //check to stop for ratio float stopRatio; @@ -345,6 +370,12 @@ static uint32_t kRed = BE(0xFF6450FF), //255, 100, 80 [progressString appendFormat: NSLocalizedString(@"%@ of %@ (%.2f%%)", "Torrent -> progress string"), [NSString stringForFileSize: [self downloadedValid]], [NSString stringForFileSize: [self size]], 100.0 * [self progress]]; + else if ([self progress] < 1.0) + [progressString appendFormat: NSLocalizedString(@"%@ of %@ (%.2f%%), uploaded %@ (Ratio: %@)", + "Torrent -> progress string"), + [NSString stringForFileSize: [self downloadedValid]], [NSString stringForFileSize: [self size]], + 100.0 * [self progress], [NSString stringForFileSize: [self uploadedTotal]], + [NSString stringForRatio: [self ratio]]]; else [progressString appendFormat: NSLocalizedString(@"%@, uploaded %@ (Ratio: %@)", "Torrent -> progress string"), [NSString stringForFileSize: [self size]], [NSString stringForFileSize: [self uploadedTotal]], @@ -439,6 +470,7 @@ static uint32_t kRed = BE(0xFF6450FF), //255, 100, 80 break; case TR_STATUS_SEED: + case TR_STATUS_DONE: [statusString setString: @""]; if ([self totalPeers] != 1) [statusString appendFormat: NSLocalizedString(@"Seeding to %d of %d peers", "Torrent -> status string"), @@ -518,13 +550,12 @@ static uint32_t kRed = BE(0xFF6450FF), //255, 100, 80 [self name], @"Name", [NSNumber numberWithBool: [self isSeeding]], @"Seeding", [NSNumber numberWithFloat: [self progress]], @"Progress", + [NSNumber numberWithFloat: (float)fStat->left/[self size]], @"Left", [NSNumber numberWithBool: [self isActive]], @"Active", [NSNumber numberWithBool: [self isError]], @"Error", nil]; if ([self isSeeding]) - { [info setObject: [NSNumber numberWithFloat: [self progressStopRatio]] forKey: @"ProgressStopRatio"]; - } if (![fDefaults boolForKey: @"SmallView"]) { @@ -611,11 +642,6 @@ static uint32_t kRed = BE(0xFF6450FF), //255, 100, 80 tr_torrentRemoveFastResume(fHandle); } -- (BOOL) allDownloaded -{ - return [self progress] >= 1.0; -} - - (float) ratio { return fStat->ratio; @@ -1042,6 +1068,7 @@ static uint32_t kRed = BE(0xFF6450FF), //255, 100, 80 break; case TR_STATUS_SEED: + case TR_STATUS_DONE: return NSLocalizedString(@"Seeding", "Torrent -> status string"); break; @@ -1056,7 +1083,7 @@ static uint32_t kRed = BE(0xFF6450FF), //255, 100, 80 - (float) progress { - return fStat->progress; + return fStat->percentComplete; } - (int) eta @@ -1071,7 +1098,7 @@ static uint32_t kRed = BE(0xFF6450FF), //255, 100, 80 - (BOOL) isSeeding { - return fStat->status == TR_STATUS_SEED; + return fStat->status == TR_STATUS_SEED || fStat->status == TR_STATUS_DONE; } - (BOOL) isPaused @@ -1089,6 +1116,11 @@ static uint32_t kRed = BE(0xFF6450FF), //255, 100, 80 return fStat->status == TR_STATUS_CHECK; } +- (BOOL) allDownloaded +{ + return fStat->cpStatus != TR_CP_INCOMPLETE; +} + - (BOOL) isError { return fStat->error != 0; @@ -1107,11 +1139,6 @@ static uint32_t kRed = BE(0xFF6450FF), //255, 100, 80 return error; } -- (BOOL) justFinished -{ - return tr_getFinished(fHandle); -} - - (NSArray *) peers { int totalPeers, i; @@ -1237,7 +1264,7 @@ static uint32_t kRed = BE(0xFF6450FF), //255, 100, 80 - (uint64_t) downloadedValid { - return fInfo->totalSize - fStat->left; + return fStat->downloadedValid; } - (uint64_t) downloadedTotal @@ -1289,88 +1316,94 @@ static uint32_t kRed = BE(0xFF6450FF), //255, 100, 80 return fInfo->fileCount; } -- (BOOL) updateFileProgress +- (float) fileProgress: (int) index { - BOOL change = NO; - float * progress = tr_torrentCompletion(fHandle); - NSNumber * progressNum; - NSMutableDictionary * item, * dict; - - int i, fileCount = [self fileCount]; - for (i = 0; i < fileCount; i++) - { - if (!(progressNum = [[fFileFlatList objectAtIndex: i] objectForKey: @"Progress"]) - || [progressNum floatValue] != progress[i]) - { - item = [fFileFlatList objectAtIndex: i]; - [item setObject: [NSNumber numberWithFloat: progress[i]] forKey: @"Progress"]; - change = YES; - - if (progress[i] == 1.0) - { - dict = item; - while ((dict = [dict objectForKey: @"Parent"])) - [dict setObject: [NSNumber numberWithInt: [[dict objectForKey: @"Remaining"] intValue]-1] - forKey: @"Remaining"]; - - [item setObject: [NSNumber numberWithInt: NSOnState] forKey: @"Check"]; - [self resetFileCheckStateForItemParent: item]; - } - } - } - - free(progress); - - return change; + return tr_torrentFileCompletion(fHandle, index); } -- (void) setFileCheckState: (int) state forFileItem: (NSMutableDictionary *) item +- (int) checkForFiles: (NSIndexSet *) indexSet { - [item setObject: [NSNumber numberWithInt: state] forKey: @"Check"]; - - if (![[item objectForKey: @"IsFolder"] boolValue]) - return; - - NSMutableDictionary * child; - NSEnumerator * enumerator = [[item objectForKey: @"Children"] objectEnumerator]; - while ((child = [enumerator nextObject])) - if (state != [[child objectForKey: @"Check"] intValue]) - [self setFileCheckState: state forFileItem: child]; + BOOL onState = NO, offState = NO; + int index; + for (index = [indexSet firstIndex]; index != NSNotFound; index = [indexSet indexGreaterThanIndex: index]) + { + if (tr_torrentGetFilePriority(fHandle, index) != TR_PRI_DND || [self fileProgress: index] >= 1.0) + onState = YES; + else + offState = YES; + + if (onState == offState) + return NSMixedState; + } + return onState ? NSOnState : NSOffState; } -- (NSMutableDictionary *) resetFileCheckStateForItemParent: (NSMutableDictionary *) originalChild +- (BOOL) canChangeDownloadCheckForFiles: (NSIndexSet *) indexSet { - NSMutableDictionary * item; - if (!(item = [originalChild objectForKey: @"Parent"])) - return originalChild; - - int state = INVALID; - - NSMutableDictionary * child; - NSEnumerator * enumerator = [[item objectForKey: @"Children"] objectEnumerator]; - while ((child = [enumerator nextObject])) + int index; + for (index = [indexSet firstIndex]; index != NSNotFound; index = [indexSet indexGreaterThanIndex: index]) + if ([self fileProgress: index] < 1.0) + return YES; + return NO; +} + +- (void) setFileCheckState: (int) state forIndexes: (NSIndexSet *) indexSet +{ + int index; + for (index = [indexSet firstIndex]; index != NSNotFound; index = [indexSet indexGreaterThanIndex: index]) { - if (state == INVALID) + tr_priority_t actualPriority; + if (state == NSOnState) { - state = [[child objectForKey: @"Check"] intValue]; - if (state == NSMixedState) - break; + int priority = [[[fFlatFileList objectAtIndex: index] objectForKey: @"Priority"] intValue]; + if (priority == PRIORITY_HIGH) + actualPriority = TR_PRI_HIGH; + else if (priority == PRIORITY_LOW) + actualPriority = TR_PRI_LOW; + else + actualPriority = TR_PRI_NORMAL; } - else if (state != [[child objectForKey: @"Check"] intValue]) - { - state = NSMixedState; - break; - } - else; + else + actualPriority = TR_PRI_DND; + + tr_torrentSetFilePriority(fHandle, index, actualPriority); } - if (state != [[item objectForKey: @"Check"] intValue]) + #warning when going seeding to download, update queue + [self update]; + [[NSNotificationCenter defaultCenter] postNotificationName: @"UpdateQueue" object: self]; //for paused torrents +} + +- (void) setFilePriority: (int) priority forIndexes: (NSIndexSet *) indexSet +{ + NSNumber * priorityValue = [NSNumber numberWithInt: priority]; + + int index; + for (index = [indexSet firstIndex]; index != NSNotFound; index = [indexSet indexGreaterThanIndex: index]) { - [item setObject: [NSNumber numberWithInt: state] forKey: @"Check"]; - return [self resetFileCheckStateForItemParent: item]; + [[fFlatFileList objectAtIndex: index] setObject: priorityValue forKey: @"Priority"]; + + if ([self checkForFiles: [NSIndexSet indexSetWithIndex: index]] == NSOnState) + { + tr_priority_t actualPriority; + if (priority == PRIORITY_HIGH) + actualPriority = TR_PRI_HIGH; + else if (priority == PRIORITY_LOW) + actualPriority = TR_PRI_LOW; + else + actualPriority = TR_PRI_NORMAL; + tr_torrentSetFilePriority(fHandle, index, actualPriority); + } } - else - return originalChild; +} + +- (BOOL) hasFilePriority: (int) priority forIndexes: (NSIndexSet *) indexSet +{ + int index; + for (index = [indexSet firstIndex]; index != NSNotFound; index = [indexSet indexGreaterThanIndex: index]) + if (priority == [[[fFlatFileList objectAtIndex: index] objectForKey: @"Priority"] intValue]) + return YES; + return NO; } - (NSDate *) dateAdded @@ -1462,6 +1495,7 @@ static uint32_t kRed = BE(0xFF6450FF), //255, 100, 80 checkDownload: (NSNumber *) checkDownload downloadLimit: (NSNumber *) downloadLimit pex: (NSNumber *) pex waitToStart: (NSNumber *) waitToStart orderValue: (NSNumber *) orderValue + filesShouldDownload: (NSArray *) filesShouldDownload filePriorities: (NSArray *) filePriorities; { if (!(self = [super init])) return nil; @@ -1536,7 +1570,7 @@ static uint32_t kRed = BE(0xFF6450FF), //255, 100, 80 fShortStatusString = [[NSMutableString alloc] initWithCapacity: 30]; fRemainingTimeString = [[NSMutableString alloc] initWithCapacity: 30]; - [self createFileList]; + [self createFileListShouldDownload: filesShouldDownload priorities: filePriorities]; //set up advanced bar fBitmap = [[NSBitmapImageRep alloc] initWithBitmapDataPlanes: nil @@ -1552,15 +1586,17 @@ static uint32_t kRed = BE(0xFF6450FF), //255, 100, 80 return self; } -- (void) createFileList +- (void) createFileListShouldDownload: (NSArray *) filesShouldDownload priorities: (NSArray *) filePriorities { int count = [self fileCount], i; tr_file_t * file; NSMutableArray * pathComponents; NSString * path; + int priority; + tr_priority_t actualPriority; NSMutableArray * fileList = [[NSMutableArray alloc] init], - * fileFlatList = [[NSMutableArray alloc] initWithCapacity: count]; + * flatFileList = [[NSMutableArray alloc] initWithCapacity: count]; for (i = 0; i < count; i++) { @@ -1575,20 +1611,35 @@ static uint32_t kRed = BE(0xFF6450FF), //255, 100, 80 else path = @""; + priority = filePriorities ? [[filePriorities objectAtIndex: i] intValue] : PRIORITY_NORMAL; [self insertPath: pathComponents forSiblings: fileList withParent: nil previousPath: path - fileSize: file->length state: NSOnState flatList: fileFlatList]; + flatList: flatFileList fileSize: file->length index: i priority: priority]; [pathComponents autorelease]; + + if (!filesShouldDownload || [[filesShouldDownload objectAtIndex: i] boolValue]) + { + if (priority == PRIORITY_HIGH) + actualPriority = TR_PRI_HIGH; + else if (priority == PRIORITY_LOW) + actualPriority = TR_PRI_LOW; + else + actualPriority = TR_PRI_NORMAL; + } + else + actualPriority = TR_PRI_DND; + + tr_torrentSetFilePriority(fHandle, i, actualPriority); } fFileList = [[NSArray alloc] initWithArray: fileList]; [fileList release]; - fFileFlatList = [[NSArray alloc] initWithArray: fileFlatList]; - [fileFlatList release]; + fFlatFileList = [[NSArray alloc] initWithArray: flatFileList]; + [flatFileList release]; } - (void) insertPath: (NSMutableArray *) components forSiblings: (NSMutableArray *) siblings withParent: (NSMutableDictionary *) parent previousPath: (NSString *) previousPath - fileSize: (uint64_t) size state: (int) state flatList: (NSMutableArray *) flatList + flatList: (NSMutableArray *) flatList fileSize: (uint64_t) size index: (int) index priority: (int) priority { NSString * name = [components objectAtIndex: 0]; BOOL isFolder = [components count] > 1; @@ -1608,42 +1659,48 @@ static uint32_t kRed = BE(0xFF6450FF), //255, 100, 80 if (!dict) { dict = [NSMutableDictionary dictionaryWithObjectsAndKeys: name, @"Name", - [NSNumber numberWithBool: isFolder], @"IsFolder", - currentPath, @"Path", nil]; - + [NSNumber numberWithBool: isFolder], @"IsFolder", currentPath, @"Path", nil]; [siblings addObject: dict]; if (isFolder) { [dict setObject: [NSMutableArray array] forKey: @"Children"]; - [dict setObject: [NSNumber numberWithInt: 1] forKey: @"Remaining"]; + [dict setObject: [NSMutableIndexSet indexSetWithIndex: index] forKey: @"Indexes"]; } else { - [flatList addObject: dict]; + [dict setObject: [NSIndexSet indexSetWithIndex: index] forKey: @"Indexes"]; [dict setObject: [NSNumber numberWithUnsignedLongLong: size] forKey: @"Size"]; [dict setObject: [[NSWorkspace sharedWorkspace] iconForFileType: [name pathExtension]] forKey: @"Icon"]; + [dict setObject: [NSNumber numberWithInt: priority] forKey: @"Priority"]; + + [flatList addObject: dict]; } if (parent) [dict setObject: parent forKey: @"Parent"]; - [dict setObject: [NSNumber numberWithInt: state] forKey: @"Check"]; } else - { - if (isFolder) - [dict setObject: [NSNumber numberWithInt: [[dict objectForKey: @"Remaining"] intValue]+1] forKey: @"Remaining"]; - - int dictState = [[dict objectForKey: @"Check"] intValue]; - if (dictState != NSMixedState && dictState != state) - [dict setObject: [NSNumber numberWithInt: NSMixedState] forKey: @"Check"]; - } + [[dict objectForKey: @"Indexes"] addIndex: index]; if (isFolder) { [components removeObjectAtIndex: 0]; [self insertPath: components forSiblings: [dict objectForKey: @"Children"] - withParent: dict previousPath: currentPath fileSize: size state: state flatList: flatList]; + withParent: dict previousPath: currentPath flatList: flatList fileSize: size index: index priority: priority]; + } +} + +- (void) historyFilePriorities: (NSMutableArray *) history forItems: (NSArray *) items +{ + NSEnumerator * enumerator = [items objectEnumerator]; + NSDictionary * item; + while ((item = [enumerator nextObject])) + { + if (![[item objectForKey: @"IsFolder"] boolValue]) + [history addObject: [item objectForKey: @"Priority"]]; + else + [self historyFilePriorities: history forItems: [item objectForKey: @"Children"]]; } } diff --git a/macosx/TorrentCell.h b/macosx/TorrentCell.h index 7f2e1a5b5..ccd874b9e 100644 --- a/macosx/TorrentCell.h +++ b/macosx/TorrentCell.h @@ -31,7 +31,7 @@ @interface TorrentCell : NSCell { NSImage * fErrorImage; - CTGradient * fWhiteGradient, * fGrayGradient, * fLightGreenGradient, + CTGradient * fWhiteGradient, * fGrayGradient, * fLightGrayGradient, * fLightGreenGradient, * fGreenGradient, * fBlueGradient, * fTransparentGradient; NSUserDefaults * fDefaults; } diff --git a/macosx/TorrentCell.m b/macosx/TorrentCell.m index 525114489..192cb5582 100644 --- a/macosx/TorrentCell.m +++ b/macosx/TorrentCell.m @@ -46,8 +46,9 @@ { fDefaults = [NSUserDefaults standardUserDefaults]; - fWhiteGradient = [[CTGradient aquaNormalGradient] retain]; + fWhiteGradient = [[CTGradient progressWhiteGradient] retain]; fGrayGradient = [[CTGradient progressGrayGradient] retain]; + fLightGrayGradient = [[CTGradient progressLightGrayGradient] retain]; fBlueGradient = [[CTGradient progressBlueGradient] retain]; fGreenGradient = [[CTGradient progressGreenGradient] retain]; fLightGreenGradient = [[CTGradient progressLightGreenGradient] retain]; @@ -81,25 +82,41 @@ barBounds = NSMakeRect(point.x, point.y - BAR_HEIGHT, width, BAR_HEIGHT); completeBounds = barBounds; + float progress = [[info objectForKey: @"Progress"] floatValue]; + completeBounds.size.width = progress * width; + + if (progress < 1.0) + { + [fWhiteGradient fillRect: barBounds angle: -90]; + + float left = [[info objectForKey: @"Left"] floatValue]; + if ((progress + left) < 1.0) + { + NSRect blankBounds = barBounds; + blankBounds.origin.x += width * (progress + left); + blankBounds.size.width = width * ((1.0 - progress) - left); + + [fLightGrayGradient fillRect: blankBounds angle: -90]; + } + } + if ([[info objectForKey: @"Seeding"] boolValue]) { - completeBounds.size.width = width * [[info objectForKey: @"ProgressStopRatio"] floatValue]; + NSRect ratioBounds = completeBounds; + ratioBounds.size.width *= [[info objectForKey: @"ProgressStopRatio"] floatValue]; - if (completeBounds.size.width < barBounds.size.width) - [fLightGreenGradient fillRect: barBounds angle: -90]; - [fGreenGradient fillRect: completeBounds angle: -90]; + if (ratioBounds.size.width < completeBounds.size.width) + [fLightGreenGradient fillRect: completeBounds angle: -90]; + [fGreenGradient fillRect: ratioBounds angle: -90]; } else { - completeBounds.size.width = [[info objectForKey: @"Progress"] floatValue] * width; - if (completeBounds.size.width < barBounds.size.width) - [fWhiteGradient fillRect: barBounds angle: -90]; - if ([[info objectForKey: @"Active"] boolValue]) [fBlueGradient fillRect: completeBounds angle: -90]; else [fGrayGradient fillRect: completeBounds angle: -90]; } + [[NSColor colorWithDeviceWhite: 0.0 alpha: 0.2] set]; [NSBezierPath strokeRect: NSInsetRect(barBounds, 0.5, 0.5)]; } diff --git a/macosx/TorrentTableView.m b/macosx/TorrentTableView.m index 5368dd13f..10978a28a 100644 --- a/macosx/TorrentTableView.m +++ b/macosx/TorrentTableView.m @@ -239,7 +239,6 @@ NSRect rect; Torrent * torrent; NSImage * image; - NSRect buttonRect = NSMakeRect(0, 0, BUTTON_WIDTH, BUTTON_WIDTH); [super drawRect: r]; @@ -264,13 +263,11 @@ image = nil; if (image) - [image compositeToPoint: NSMakePoint(rect.origin.x, NSMaxY(rect)) fromRect: buttonRect - operation: NSCompositeSourceOver]; + [image compositeToPoint: NSMakePoint(rect.origin.x, NSMaxY(rect)) operation: NSCompositeSourceOver]; rect = [self revealRectForRow: i]; image = NSPointInRect(fClickPoint, rect) ? fRevealOnIcon : fRevealOffIcon; - [image compositeToPoint: NSMakePoint(rect.origin.x, NSMaxY(rect)) fromRect: buttonRect - operation: NSCompositeSourceOver]; + [image compositeToPoint: NSMakePoint(rect.origin.x, NSMaxY(rect)) operation: NSCompositeSourceOver]; } } diff --git a/macosx/Transmission Help/Transmission Help.helpindex b/macosx/Transmission Help/Transmission Help.helpindex index 92f48b7608594ea226083d0905f2b761a8fdc30b..432ff0cdb302b9cf346b7b8ac06ed92c95b26f38 100644 GIT binary patch literal 31725 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 Q+yL|%{Vn6OeEIVK4axe%RsaA1 literal 33730 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 diff --git a/macosx/Transmission Help/gfx/creation.png b/macosx/Transmission Help/gfx/creation.png new file mode 100644 index 0000000000000000000000000000000000000000..c571041c00fe8dd2f628a795154e9fd73c2877fe GIT binary patch literal 15049 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 literal 0 HcmV?d00001 diff --git a/macosx/Transmission Help/gfx/fileselection.png b/macosx/Transmission Help/gfx/fileselection.png new file mode 100644 index 0000000000000000000000000000000000000000..2ba222ee43bb7d40f5899104645b4f8f7556bb38 GIT binary patch literal 41146 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 P00000NkvXXu0mjf4Ruw@ literal 0 HcmV?d00001 diff --git a/macosx/Transmission Help/html/FAQ.html b/macosx/Transmission Help/html/FAQ.html index 1cc445a84..9c8163444 100644 --- a/macosx/Transmission Help/html/FAQ.html +++ b/macosx/Transmission Help/html/FAQ.html @@ -12,6 +12,9 @@ + +

Why is my download so slow?

diff --git a/macosx/Transmission Help/html/Index2.html b/macosx/Transmission Help/html/Index2.html index 37b5792bf..825614ca9 100644 --- a/macosx/Transmission Help/html/Index2.html +++ b/macosx/Transmission Help/html/Index2.html @@ -14,7 +14,11 @@ @@ -78,6 +82,18 @@ + + +

Managing your transfers

+ + + + + +

 

+ + +

Maximizing download speed

diff --git a/macosx/Transmission Help/html/Speed.html b/macosx/Transmission Help/html/Speed.html index 3d2992f2c..d68c0624e 100644 --- a/macosx/Transmission Help/html/Speed.html +++ b/macosx/Transmission Help/html/Speed.html @@ -9,7 +9,11 @@
diff --git a/macosx/Transmission Help/html/check.html b/macosx/Transmission Help/html/check.html index 95c859941..d10740b81 100644 --- a/macosx/Transmission Help/html/check.html +++ b/macosx/Transmission Help/html/check.html @@ -11,6 +11,9 @@ + +

How do I manually recheck my files?

@@ -24,18 +27,9 @@

To do this:

    -
  1. In Transmission, click on the magnifying glass next to the torrent in question. -

    This should open a Finder window which displays all the files in your torrent.

  2. -
  3. Select the torrent in Transmission and click "Remove". -

    NB: make sure you keep a copy of the torrent file - you'll need it later!

  4. -
  5. Open the Terminal. You can find it in /Applications/Utilities.
  6. -
  7. Once inside the Terminal, type "touch " (including a space). Do not hit Enter yet.
  8. -
  9. Drag any one of the files you are downloading into the Terminal window (from the Finder window you opened in step 1). -

    The file path should automagically appear.

  10. -
  11. Now hit "Enter" inside the terminal.
  12. -
  13. Quit Terminal.
  14. -
  15. Reload the torrent file in question into Transmission. -

    Transmission should start rechecking your files!

  16. +
  17. Select the relevant torrent.
  18. +
  19. Pause it, and then go to the Transfers menu >> Remove fast resume cache.
  20. +
  21. Start the torrent again. Transmission will check it first.
diff --git a/macosx/Transmission Help/html/gettingstarted.html b/macosx/Transmission Help/html/gettingstarted.html index 3ffef9d8d..8f463a57c 100644 --- a/macosx/Transmission Help/html/gettingstarted.html +++ b/macosx/Transmission Help/html/gettingstarted.html @@ -10,11 +10,18 @@
+
+ Adium X icon +

Welcome to Transmission!

@@ -28,12 +35,32 @@ - + -

You'll need to download a torrent file (extension .torrent). These are commonly found at 'tracker' websites. Torrent files contain information about the actual file you want to download (eg a movie). -

Once you have the torrent file, open it in Transmission - downloading should start immediately. Transmission can even watch for torrent files and then open them automatically via Preferences >> Transfers >> General. -

You can pause and resume transfers at any time, so long as the files remain in your download folder. If you remove a transfer, in order to resume it you will need to reopen the original torrent file in Transmission. -

It is good etiquette to share or 'seed' the file for a while (ie leave it uploading) once your download is complete. You can set a default ratio to automatically seed to, and then pause. This can be adjusted in Preferences >> Transfers >> Management, or in real time using the Action menu.

+

Download your file's associated 'torrent file' (extension .torrent). These are commonly found at 'tracker' websites. +

Once you have the torrent file, drag it into Transmission - downloading should start immediately. +

You can pause and resume transfers at any time, so long as the files remain in your download folder. +

It is good etiquette to share or 'seed' the file for a while (ie leave it uploading) once your download is complete. +

+ + + +
+ + + + + + +
+

Can I create my own torrents?

+
+

Yes, you can share a file or folder by dragging it into Transmission. Alternatively, click 'Create' in the toolbar, and choose your file. +

When the dialogue box appears, enter your tracker address, comments and private status. +

You can change the torrent filename, as well as where it will be saved to by clicking 'Change'. +

Once you are done, click 'Create'. Transmission will automatically optimize the torrent file for what you are sharing.

@@ -92,6 +119,24 @@
+ + + + + + + + + +
+

Can I choose do download specific files?

+
+

Yes, via the Inspector. Double click any transfer to open it and then click the 'Files' tab. Simply check the boxes next to the files you want to download (the default is all of them). + You can even set a priority (red/high or yellow/low) to each file, if you want some to finish faster than others. To do so, use the selector next to the checkboxes. +

If you are frequently going to selectively download the files in your torrents, you might want to disable "Start Transfers when added" in Preferences >> Transfers >> Management. + This way you can organise your transfer before it starts, to avoid wasting download bandwidth. +

+
diff --git a/macosx/Transmission Help/html/multiple.html b/macosx/Transmission Help/html/multiple.html index f88db81b1..8003b8fd8 100644 --- a/macosx/Transmission Help/html/multiple.html +++ b/macosx/Transmission Help/html/multiple.html @@ -9,7 +9,11 @@
diff --git a/macosx/Transmission Help/html/pffirewall.html b/macosx/Transmission Help/html/pffirewall.html index 7b86cca80..95aa420be 100644 --- a/macosx/Transmission Help/html/pffirewall.html +++ b/macosx/Transmission Help/html/pffirewall.html @@ -9,7 +9,11 @@
diff --git a/macosx/Transmission Help/html/pfrouter.html b/macosx/Transmission Help/html/pfrouter.html index a1012fb42..07235c836 100644 --- a/macosx/Transmission Help/html/pfrouter.html +++ b/macosx/Transmission Help/html/pfrouter.html @@ -9,7 +9,11 @@
diff --git a/macosx/Transmission Help/html/portforward.html b/macosx/Transmission Help/html/portforward.html index d640c69fc..8721ece2f 100644 --- a/macosx/Transmission Help/html/portforward.html +++ b/macosx/Transmission Help/html/portforward.html @@ -6,18 +6,18 @@ Port Forwarding FAQ -
+

Why do I see a red dot and "Port is closed"?

-

You haven't port forwarded correctly. Port forwarding opens a port in your firewall or router so that incoming connections from the outside world can be made with Transmission. +

You haven't port forwarded correctly. Port forwarding opens a port in your firewall or router so that incoming connections from the outside world can be made with Transmission. If the port is forwarded, other people in the torrent can see you, thus increasing your potential number of connections, which more importantly, may increase the speed of your download.

-

Why do I need to Port Forward?

+

When do I need to Port Forward?

-

Transmission uses a single port for all the torrents you are downloading. If the port has not been opened (ie forwarded) that means only you can make connections to others in the swarm. - If the port is forwarded, others can connect to you, thus increasing the potential number of people connected to you, and more importantly increasing the speed of your download. +

If you share your internet connection with a router, or if your broadband modem is a router itself, then you will need to forward Transmission's port by following the instructions below. In addition, if you have enabled the Mac OS X firewall, you will need to open a port in that too. See below for details. +

How do I Port Forward?

@@ -34,11 +34,9 @@
  • If you don't have a compatible router, it is simple to forward Transmission's port manually. For instructions click here.
  • - -
  • If you don't use a router, that is, your modem is directly connected to your computer, you'll need to open Transmission's port in the Mac OS X firewall. For instructions click here.
    Keep in mind that many DSL modems also function as routers, and hence port forwarding as per above may still be necessary, even though your computer is directly connected to the modem.
    - NB: it is highly recommended you enable the Mac OS X firewall if you are not using a router.
  • - - +
  • If you don't use a router, that is, your modem is directly connected to your computer, you'll need to open Transmission's port in the Mac OS X firewall. For instructions click here.
    NB: it is highly recommended you enable the Mac OS X firewall if you are not using a router.
  • +

    Keep in mind that many DSL modems also function as routers, and hence port forwarding as per above may still be necessary, even though your computer is directly connected to the modem. +

    How do I know if I've done it right?

    diff --git a/macosx/Transmission Help/html/upnp.html b/macosx/Transmission Help/html/upnp.html index 8bef0342a..250956f6f 100644 --- a/macosx/Transmission Help/html/upnp.html +++ b/macosx/Transmission Help/html/upnp.html @@ -9,7 +9,11 @@
    @@ -23,6 +27,7 @@

    If you are still having problems, open the Message Log (in the Window menu) and post the debug output on the support forums. + Make sure you pause your torrents first; then clear the log, and toggle "Automatically forward port". Post this output.

    Airport

    diff --git a/macosx/Transmission Help/html/usingt.html b/macosx/Transmission Help/html/usingt.html new file mode 100644 index 000000000..a7cacbe45 --- /dev/null +++ b/macosx/Transmission Help/html/usingt.html @@ -0,0 +1,42 @@ + + + + + + Using Transmission + + +
    + + +
    +
    +

    Managing your transfers

    +
    +
      +
    • Torrent files contain information about the actual file you want to download (eg a movie), and connect you to the swarm of peers sharing it.
    • + +
    • Transmission can watch a certain folder (eg your Safari download folder) for torrent files and then open them automatically via Preferences >> Transfers >> General.
    • + +
    • By default, Transmission deletes the original torrent file upon opening. If you remove a transfer, in order to resume it you will need to reopen the original torrent file in Transmission. + Simply choose 'Save a Torrent Copy as' from the File menu before deletion to avoid having to download the torrent file again.
    • + +
    • Once your download is complete, you can set a default ratio to automatically seed to, and then pause. + This can be adjusted in Preferences >> Transfers >> Management, or in real time using the Action menu.
    • + +
    • Both seeding and downloading transfers can be queued, and Transmission can skip over stalled transfers, in order to maximise queuing efficiency. + Queuing can be configured in Preferences >> Transfers >> Management.
    • +
    + + +
    + + + \ No newline at end of file diff --git a/mk/gtk.mk b/mk/gtk.mk index 1e90608b7..05dce612b 100644 --- a/mk/gtk.mk +++ b/mk/gtk.mk @@ -3,10 +3,9 @@ include ../mk/config.mk include ../mk/common.mk -SRCS = conf.c dialogs.c hig.c io.c ipc.c main.c msgwin.c \ - torrent-inspector.c \ - tr_cell_renderer_progress.c tr_core.c tr_icon.c tr_prefs.c \ - tr_torrent.c tr_window.c util.c +SRCS = actions.c conf.c dialogs.c hig.c io.c ipc.c main.c msgwin.c \ + makemeta-ui.c torrent-inspector.c tr_cell_renderer_progress.c \ + tr_core.c tr_icon.c tr_prefs.c tr_torrent.c tr_window.c util.c OBJS = $(SRCS:%.c=%.o) CFLAGS += $(CFLAGS_GTK) -I../libtransmission diff --git a/mk/lib.mk b/mk/lib.mk index 77dbed08b..e7ef58550 100644 --- a/mk/lib.mk +++ b/mk/lib.mk @@ -3,10 +3,10 @@ include ../mk/config.mk include ../mk/common.mk -SRCS = transmission.c bencode.c net.c tracker.c peer.c inout.c \ - metainfo.c sha1.c utils.c fdlimit.c clients.c completion.c \ - platform.c ratecontrol.c choking.c natpmp.c upnp.c http.c xml.c \ - shared.c torrent.c strlcpy.c strlcat.c ipcparse.c +SRCS = bencode.c choking.c clients.c completion.c fastresume.c fdlimit.c \ + http.c inout.c ipcparse.c makemeta.c metainfo.c natpmp.c net.c \ + peer.c platform.c ratecontrol.c sha1.c shared.c strlcat.c strlcpy.c \ + torrent.c tracker.c transmission.c upnp.c utils.c xml.c OBJS = $(SRCS:%.c=%.o)