"""
FtpCube
Copyright (C) Michael Gilfix
This file is part of FtpCube.
You should have received a file COPYING containing license terms
along with this program; if not, write to Michael Gilfix
(mgilfix@eecs.tufts.edu) for a copy.
This version of FtpCube is open source; you can redistribute it and/or
modify it under the terms listed in the file COPYING.
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.
"""
import archiver
import events
import icons.check_archive
import icons.clear
import icons.load_queue
import icons.main_thread
import icons.resubmit_job
import icons.save_queue
import icons.thread_idle
import icons.thread_connect
import icons.thread_listing
import icons.thread_abort
import icons.thread_action
import icons.thread_download
import messages
import protocol
import threads
import txtwrapper
import utils
import wx
import os
import pickle
class ControlWindow(wx.Panel):
"""The transfer control window.
This window is the heart of controlling file transferring. It provides a UI for the
transfer queue, the thread manager, the transfers, and failures. The control window
holds a notebook containing a tab for each of the UI function categories. It also
contains a status part that describes the size of the transfer queue."""
def __init__(self, parent):
"""Creates the control window."""
if __debug__:
print "Making the control window."
wx.Panel.__init__(self, parent, -1, style=wx.NO_BORDER)
subsizer = self.makeStatusBar()
# Create the notebook contents
self.notebook = wx.Notebook(self, -1)
self.queue_tab = QueueTab(self.notebook)
self.notebook.AddPage(self.queue_tab, _("Queue"))
self.threads_tab = ThreadsTab(self.notebook)
self.notebook.AddPage(self.threads_tab, _("Threads"))
self.download_tab = DownloadTab(self.notebook)
self.notebook.AddPage(self.download_tab, _("Downloads"))
self.failure_tab = FailureTab(self.notebook)
self.notebook.AddPage(self.failure_tab, _("Failures"))
# Start with the threads tab
self.notebook.SetSelection(1)
sizer = wx.BoxSizer(wx.VERTICAL)
sizer.Add(self.notebook, 1, wx.EXPAND)
sizer.Add(subsizer, 0, wx.EXPAND)
self.SetAutoLayout(True)
self.SetSizer(sizer)
# Register ourselves to update the queue status bar
evt_registry = events.getEventRegistry()
self.Bind(events.EVT_QUEUE, self.onQueueEvent)
evt_registry.registerEventListener(events.EVT_QUEUE_TYPE, self)
self.Bind(events.EVT_DEQUEUE, self.onDequeueEvent)
evt_registry.registerEventListener(events.EVT_DEQUEUE_TYPE, self)
def makeStatusBar(self):
"""Creates the queue progress status bar."""
self.queue_label = wx.StaticText(self, -1, _("%(cnt)d Queued") %{ 'cnt' : 0 })
self.queue_progress = wx.Gauge(self, -1, 1, size=(-1, 3))
# Start with no progress and with the progress bar hidden
self.progress = (0, 0)
self.queue_progress.Show(False)
sizer = wx.FlexGridSizer(3, 5)
sizer.AddMany([
((0, 3)), ((0, 3)), ((0, 3)), ((0, 3)), ((0, 3)),
((10, 0)),
(self.queue_label, 0, wx.EXPAND),
((10, 0)),
(self.queue_progress, 0, wx.EXPAND),
((10, 0)),
((0, 3)), ((0, 3)), ((0, 3)), ((0, 3)), ((0, 3)),
])
sizer.AddGrowableCol(2)
sizer.AddGrowableRow(1)
return sizer
def getQueueTab(self):
"""Returns the queue tab."""
return self.queue_tab
def getThreadsTab(self):
"""Returns the thread tab."""
return self.threads_tab
def getDownloadTab(self):
"""Returns the transfered tab."""
return self.download_tab
def getFailureTab(self):
"""Returns the failure tab."""
return self.failure_tab
def onQueueEvent(self, event):
"""Processes a queue event.
This updates the progress bar and text to indicate that a new item has been queued.
Adding the item to the queue display is handled elsewhere."""
if not self.queue_progress.IsShown():
self.queue_progress.Show(True)
self.Layout()
else:
self.queue_progress.SetRange(self.queue_progress.GetRange() + 1)
queued = self.queue_progress.GetRange() - self.queue_progress.GetValue()
self.queue_label.SetLabel(_("%(cnt)d Queued") %{ 'cnt' : queued })
def onDequeueEvent(self, event):
"""Processes a dequeue event.
This updates the progress bar and text to indicate that an item has been removed
from the import queue. Removal of the item from the queue display is handled
elsewhere."""
self.queue_progress.SetValue(self.queue_progress.GetValue() + 1)
queued = self.queue_progress.GetRange() - self.queue_progress.GetValue()
self.queue_label.SetLabel(_("%(cnt)d Queued") %{ 'cnt' : queued })
if self.queue_progress.GetValue() == self.queue_progress.GetRange():
self.hideProgressBar()
def hideProgressBar(self):
"""Hides the progress bar and resets the range to a max of a single item."""
self.queue_progress.SetValue(0)
self.queue_progress.SetRange(1)
self.queue_progress.Show(False)
class QueueTab(wx.Panel):
"""The transfer queue tab.
The transfer queue tab holds entries waiting to be processed by connection threads.
As there are a fixed number of threads, the queue tab can build up over time if
files are continually being added. The contents of the queue can be saved to a file
and later loaded to resume processing. The queue can also be paused. When in a paused
state, entries can be added into the queue but not dequeued. This is particularly
useful for saving the queue to a file.
The queue can contain entries that span multiple hosts, ports, and transports. Each
entry in the queue is associated with the full event that describes how to process
the queued item."""
idTOOL_ENABLE = wx.NewId()
idTOOL_CLEAR = wx.NewId()
idTOOL_LOAD = wx.NewId()
idTOOL_SAVE = wx.NewId()
idQUEUE_LIST = wx.NewId()
def __init__(self, parent):
"""Creates the queue tab and initializes the list to empty."""
if __debug__:
print "Making queuing panel."
wx.Panel.__init__(self, parent, -1)
self.headers = [ _("Host"), _("Filename"), _("Size"), _("Attempt") ]
self.toolbar = self.makeToolBar()
self.list = wx.ListCtrl(self, self.idQUEUE_LIST,
style=wx.LC_REPORT | wx.SUNKEN_BORDER)
for col, name in zip(range(len(self.headers)), self.headers):
self.list.InsertColumn(col, name)
self.list.SetColumnWidth(0, 80)
self.list.SetColumnWidth(1, 150)
self.list.SetColumnWidth(2, 80)
self.list.SetColumnWidth(3, 60)
sizer = wx.BoxSizer(wx.VERTICAL)
sizer.Add(self.toolbar, 0, wx.EXPAND)
sizer.Add((0, 2))
sizer.Add(self.list, 1, wx.EXPAND)
self.SetAutoLayout(True)
self.SetSizer(sizer)
# Initialize instance variables
self.event_list = [ ]
self.disabled = False
self.Bind(events.EVT_QUEUE, self.onQueueEvent)
evt_registry = events.getEventRegistry()
evt_registry.registerEventListener(events.EVT_QUEUE_TYPE, self)
def makeToolBar(self):
"""Creates the queue tab toolbar."""
toolbar = wx.ToolBar(self, -1)
toolbar.SetToolBitmapSize(wx.Size(20, 20))
bitmap = icons.thread_idle.getBitmap()
toolbar.AddTool(self.idTOOL_ENABLE, bitmap, isToggle=True,
shortHelpString=_("Toggle Queuing Process"))
utils.addLineSeparator(toolbar, 12)
bitmap = icons.clear.getBitmap()
toolbar.AddTool(self.idTOOL_CLEAR, bitmap, shortHelpString=_("Clear Queue"))
utils.addLineSeparator(toolbar, 12)
bitmap = icons.load_queue.getBitmap()
toolbar.AddTool(self.idTOOL_LOAD, bitmap, shortHelpString=_("Load Queue"))
bitmap = icons.save_queue.getBitmap()
toolbar.AddTool(self.idTOOL_SAVE, bitmap, shortHelpString=_("Save Queue"))
self.Bind(wx.EVT_TOOL, self.onToggle, id=self.idTOOL_ENABLE)
self.Bind(wx.EVT_TOOL, self.onClear, id=self.idTOOL_CLEAR)
self.Bind(wx.EVT_TOOL, self.onLoad, id=self.idTOOL_LOAD)
self.Bind(wx.EVT_TOOL, self.onSave, id=self.idTOOL_SAVE)
toolbar.Realize()
return toolbar
def onQueueEvent(self, event):
"""Processes an event to add a new item to the queue."""
if __debug__:
print "Added item to the transfer queue: [%s:%s - %s]" \
%(event.host, event.port, event.file)
self.addListItem(event.host, event.file, event.size, event.attempt)
self.event_list.append(event)
def addListItem(self, host, file, size, attempt):
"""Adds an entry to the list control display."""
index = self.list.GetItemCount()
self.list.InsertStringItem(index, host)
self.list.SetStringItem(index, 1, file)
self.list.SetStringItem(index, 2, str(size))
self.list.SetStringItem(index, 3, str(attempt))
def getListItem(self, index):
"""Returns an item from the list in list form, where the entries match up to the
list columns."""
entry = [ ]
for i in range(4):
entry.append(utils.getColumnText(self.list, index, i))
return entry
def onToggle(self, event):
"""Toggle the enabling/disabling of the queue."""
self.disabled = bool((self.disabled + 1) % 2)
def isProcessingDisabled(self):
"""Returns a boolean indicating whether queue processing is disabled."""
return self.disabled
def onClear(self, event):
"""Clears the queue."""
self.event_list = [ ]
self.list.Freeze()
self.list.DeleteAllItems()
self.list.Thaw()
# Make sure the progress bar is hidden
ctrl_win = utils.getControlWindow()
ctrl_win.hideProgressBar()
def onSave(self, event):
"""Saves the contents of the list to a file for later reloading."""
path = messages.displayFileDialog(self, _("Ftpcube - Save Queue to File"), style=wx.SAVE)
if not path:
return
file = None
try:
try:
file = open(path, 'w')
# Convert to non-SWIG objects so they can be easily pickled
evt_list = [ x.toHash() for x in self.event_list if x ]
pickle.dump(evt_list, file)
except Exception, strerror:
messages.displayErrorDialog(self,
_("Error writing queue data to file: %(err)s") %{ 'err' : strerror })
return
finally:
if file:
try:
file.close()
finally:
pass
def onLoad(self, event):
"""Loads the contents of a queue and adds the contents to the list."""
path = messages.displayFileDialog(self, _("Ftpcube - Load Queue From File"))
if not path:
return
file = None
loaded = [ ]
try:
try:
file = open(path, 'r')
except Exception, strerror:
messages.displayErrorDialog(self,
_("Error reading queue data from file: %(err)s") %{ 'err' : strerror })
return
loaded = pickle.load(file)
finally:
if file:
try:
file.close()
finally:
pass
# Convert back from hash to event form
evt_list = [ events.EnqueueEvent(**x) for x in loaded ]
for event in evt_list:
self.addListItem(event.host, event.file, event.size, event.attempt)
self.event_list.extend(evt_list)
def getSize(self):
"""Returns the length of the queue."""
return len(self.event_list)
def peakNextItem(self):
"""Peaks at the next event in the list."""
item = None
if self.event_list:
item = self.event_list[0]
return item
def getNextItem(self):
"""Gets the next item from the queue."""
event = None
if self.event_list:
event = self.event_list.pop(0)
self.list.DeleteItem(0)
dequeue_evt = events.DequeueEvent(**event.toHash())
evt_registry = events.getEventRegistry()
evt_registry.postEvent(dequeue_evt)
return event
class ThreadsTab(wx.Panel):
"""Transfer thread tab.
This tab displays all of the active transfer threads, with the main browser thread as the
first entry. Each threads activities are displayed here, updated through thread related
events. Right clicking on a thread brings up a popup menu for actions to take on that
thread. The size of the transfer thread pool can be changed on this window. Threads are
created as needed, provided that all threads in the pool are busy doing work and the
maximum size of the thread pool has not been reached.
A wx timer instance is scheduled to run every interval and update the static in the
thread window. Timer events are scheduled in the main GUI thread and thus do not
have any locking/threading issues."""
idTOOL_MAINTHREAD = wx.NewId()
idTIMER = wx.NewId()
idNO_THREADS = wx.NewId()
WAKUP_TIME = 1000
def __init__(self, parent):
"""Creates the thread tab and initializes a blank thread list."""
if __debug__:
print "Making thread panel."
wx.Panel.__init__(self, parent)
self.toolbar = self.makeToolBar()
self.thread_label = wx.StaticText(self, -1, '', style=wx.ALIGN_CENTER)
self.speed_label = wx.StaticText(self, -1, '', style=wx.ALIGN_CENTER)
self.updateTransferTotals(0, 0) # Set the defaults
box = wx.StaticBox(self, -1, '')
self.thread_win = wx.ScrolledWindow(self, -1, style=wx.NO_BORDER | wx.VSCROLL)
self.thread_sizer = wx.BoxSizer(wx.VERTICAL)
self.thread_win.SetAutoLayout(True)
self.thread_win.SetSizer(self.thread_sizer)
bsizer = wx.StaticBoxSizer(box, wx.VERTICAL)
bsizer.Add(self.thread_win, 1, wx.EXPAND | wx.ALL)
# Set up the master sizer
sizer = wx.BoxSizer(wx.VERTICAL)
sizer.Add(self.toolbar, 0, wx.EXPAND)
sizer.Add((0, 10))
sizer.Add(self.thread_label, 0, wx.EXPAND)
sizer.Add(self.speed_label, 0, wx.EXPAND)
sizer.Add((0, 5))
sizer.Add(bsizer, 1, wx.EXPAND)
sizer.Add((0, 10))
sizer.SetSizeHints(self)
self.SetAutoLayout(True)
self.SetSizer(sizer)
# Initialize instance variables
self.thread_list = [ ]
self.main_thread_only = False
# Set up the idle timer
self.timer = wx.Timer(self, self.idTIMER)
self.Bind(wx.EVT_TIMER, self.onTimer, id=self.idTIMER)
self.timer.Start(self.WAKUP_TIME)
self.Bind(events.EVT_THREAD, self.onThreadEvent)
thread_mgr = utils.getAppThreadManager()
thread_mgr.addListener(self)
def makeToolBar(self):
"""Creates the thread tab toolbar."""
config = utils.getApplicationConfiguration()
toolbar = wx.ToolBar(self, -1)
toolbar.SetToolBitmapSize(wx.Size(20, 20))
self.no_threads = wx.SpinCtrl(toolbar, self.idNO_THREADS, size=wx.Size(50, -1))
self.no_threads.SetRange(1, 16)
if config.has_key('max_threads'):
self.no_threads.SetValue(config['max_threads'])
toolbar.AddControl(self.no_threads)
self.Bind(wx.EVT_SPINCTRL, self.onNoThreadUpdate, id=self.idNO_THREADS)
toolbar.AddSeparator()
label = wx.StaticText(toolbar, -1, _("max threads"))
toolbar.AddControl(label)
utils.addLineSeparator(toolbar, 12)
bitmap = icons.main_thread.getBitmap()
toolbar.AddTool(self.idTOOL_MAINTHREAD, bitmap, isToggle=True,
shortHelpString=_("Only use main thread for transfers"))
self.Bind(wx.EVT_TOOL, self.onMainThreadToggle, id=self.idTOOL_MAINTHREAD)
toolbar.Realize()
return toolbar
def onNoThreadUpdate(self, event):
"""Handles an update to the size of the transfer thread pool."""
value = self.no_threads.GetValue()
config = utils.getApplicationConfiguration()
config['max_threads'] = value
def onMainThreadToggle(self, event):
"""Toggles using only the main thread for transfers."""
self.main_thread_only = bool((self.main_thread_only + 1) % 2)
def useMainThreadOnly(self):
"""Returns a boolean indicating whether to only use the main thread for transfers."""
return self.main_thread_only
def getQueue(self):
ctrl_win = utils.getControlWindow()
return ctrl_win.getQueueTab()
def onTimer(self, event):
"""Handles a timer event.
This performs several housekeeping events. First, it checks to see if any items are on
the transfer queue and whether any threads are available to accept new work. Next, it
updates the status of all active threads in the list. It removes any finished threads
from the display."""
queue = self.getQueue()
while not queue.isProcessingDisabled() and queue.getSize() > 0:
thread = self.findAvailableThread()
if not thread:
break
event = queue.getNextItem()
self.processEvent(thread, event)
total_rate = 0.0
for t in self.thread_list:
if not t.busy():
t.updateIdle()
if t.finished():
self.removeThreadEntry(t.getId())
elif t.isTransfering():
t.updateTransferEstimate()
entry_thread = t.getEntryThread()
total_rate = total_rate + entry_thread.getTransferSpeed()
no_threads = len(self.thread_list)
self.updateTransferTotals(no_threads, total_rate)
def processEvent(self, thread, event):
"""Processes a queued event."""
real_thread = thread.getEntryThread()
if event.flags[0] in '-f':
real_thread.initiateTransfer(event)
elif event.flags[0] == 'd':
real_thread.recurseDirectory(event)
else:
real_thread.recurseLink(event)
def findAvailableThread(self):
"""Finds the next non-busy thread for the next item in the queue.
This method peeks at the queue to ensure that the next available thread is an
appropriate match. A thread is considered available if it connected to the same
host, port, and transport type, and it is not currently busy doing work. This
method takes into account whether only the main thread can be used for
performing transfers."""
thread = None
thread_mgr = utils.getAppThreadManager()
queue = self.getQueue()
event = queue.peakNextItem()
try:
if self.useMainThreadOnly():
entry = [ x for x in self.thread_list if x.getId() == 0 ]
entry = entry.pop(0)
if not entry.busy():
if entry.getHost() == event.host and entry.getPort() == event.port and \
entry.getTransport() == event.transport:
thread = entry
elif len(self.thread_list) < self.no_threads.GetValue() and \
self.withinLoginLimit(event.host):
thread = self.createThread(event)
else:
entries = [ x for x in self.thread_list if x.getId() != 0 ]
for t in entries:
if not t.busy():
if t.getHost() == event.host and t.getPort() == event.port and \
t.getTransport() == event.transport:
thread = t
else:
t.onCancelThread(None)
break
if thread is None and len(self.thread_list) < self.no_threads.GetValue() and \
self.withinLoginLimit(event.host):
thread = self.createThread(event)
finally:
return thread
def withinLoginLimit(self, host):
"""Returnns a boolean indicating whether the current number of connections to the
specified host are under the login limit."""
host = host.lower()
host_list = [ x.getEntryThread().getHost().lower() for x in self.thread_list ]
cnt = host_list.count(host)
if cnt:
thread = self.thread_list[host_list.index(host)].getEntryThread()
thread_opts = thread.getOptions()
max_logins = thread_opts['limit']
if max_logins and max_logins <= cnt:
return False
return True
def createThread(self, event):
"""Creates a transfer thread and assigns it the specified event as work."""
thread_mgr = utils.getAppThreadManager()
config = utils.getApplicationConfiguration()
opts = { }
opts.update(config.getOptions())
opts.update(event.toHash())
clogger, dlogger, ulogger = utils.getLoggers()
try:
new_id = thread_mgr.newTransferThread(opts=opts, logger=None,
download_logger=dlogger, upload_logger=ulogger)
new_thread = thread_mgr.getThread(new_id)
new_thread.saveTransferState(event)
except Exception, strerror:
if __debug__:
print "Error creating new transfer thread: %s" %strerror
return None
# Make sure we receive the new event
self.timer.Stop()
wx.Yield()
thread_list = [ x for x in self.thread_list if x.getId() == new_id ]
self.timer.Start(self.WAKUP_TIME)
if __debug__:
print "Newly created transfer thread: %s" %thread_list
thread_panel = thread_list.pop(0)
# Start the connection
entry = thread_panel.getEntryThread()
entry.initiateConnect()
return thread_panel
def onThreadEvent(self, event):
"""Processes a thread-related event."""
thread_ids = [ x.getId() for x in self.thread_list ]
# Check if the thread exists
try:
index = thread_ids.index(event.id)
except ValueError:
index = None
# Post the event to the appropriate thread GUI piece if the thread exists
if index is not None:
wx.PostEvent(self.thread_list[index], event)
return
# Otherwise, let's handle the event ourselves
if event.kind == events.ThreadEvent.EVT_CREATE:
self.addThreadEntry(event.id)
elif event.kind == events.ThreadEvent.EVT_DESTROY:
self.removeThreadEntry(event.id)
def addThreadEntry(self, id):
"""Adds a new thread entry with the specified ID.
A new thread entry widget is created for the thread and added into the thread list."""
entry = ThreadEntry(self.thread_win, id)
self.thread_list.append(entry)
self.thread_sizer.Add(entry)
self.thread_sizer.Layout()
def removeThreadEntry(self, id):
"""Removes the thread entry widget for the thread with the specified ID."""
found = [ x for x in self.thread_list if x.getId() == id ]
if found:
thread = found.pop(0)
self.thread_list.remove(thread)
self.thread_sizer.Remove(thread)
thread.Destroy()
self.thread_sizer.Layout()
def updateTransferTotals(self, threads, speed):
"""Updates the total calculated transfer speed across all threads."""
self.thread_label.SetLabel(_("%(no)d Threads Running") %{ 'no' : threads })
self.speed_label.SetLabel(_("Total Speed: %(kb)0.02f KB/s") %{ 'kb' : speed })
self.Layout()
class ThreadEntry(wx.Panel):
"""Panel widget entry in the thread list.
Each thread entry corresponds to a transfer thread. The panel contains an icon that
represents the currently executing action in the transfer thread. The status text is
updated with a description of the action taken in the thread. All text in this thread
entry box must be wrapped to fit the size of the window, otherwise it will cause the
parent thread list scrolled window to expand horizontally."""
idCANCEL_TRANSFER = wx.NewId()
idCANCEL_THREAD = wx.NewId()
idTOGGLE_TIMEOUT = wx.NewId()
def __init__(self, parent, thread_id):
"""Creates a new thread entry widget and associates it with the transfer thread with
the specified ID."""
if __debug__:
print "Making thread entry status box."
wx.Panel.__init__(self, parent, -1)
# Set instance variables
self.thread_id = thread_id
self.thread = self.getEntryThread()
self.loadBitmaps()
self.progress_bar = wx.Gauge(self, -1, 1, size=wx.Size(200, 10))
self.image = wx.StaticBitmap(self, -1, self.idle_bitmap, size=wx.Size(20, 20))
name = ''
if self.getThreadName():
label_text = "%s\n" %self.getThreadName() + _("Initializing Thread...")
else:
label_text = _("Initializing Thread...") + "\n"
self.label = txtwrapper.StaticWrapText(self, -1, '')
self.label.SetLabel(label_text)
# Create the line and ensure that it stretches the width of the parent
pwidth, pheight = parent.GetSizeTuple()
self.line = wx.StaticLine(self, -1, size=(pwidth, -1))
sizer = wx.BoxSizer(wx.HORIZONTAL)
sizer.Add((1, 5), 1, wx.EXPAND)
sizer.Add(self.progress_bar, 0, wx.EXPAND)
sizer.Add((1, 5), 1, wx.EXPAND)
self.psizer = wx.BoxSizer(wx.VERTICAL)
self.psizer.Add((1, 5), 0, wx.EXPAND)
self.psizer.Add(sizer, 0, wx.EXPAND)
self.psizer.Add((1, 5), 0, wx.EXPAND)
self.lsizer = wx.BoxSizer(wx.HORIZONTAL)
self.lsizer.Add((5, 1), 0, wx.EXPAND)
self.lsizer.Add(self.image, 0, wx.ALIGN_CENTER_VERTICAL)
self.lsizer.Add((10, 1), 0, wx.EXPAND)
self.lsizer.Add(self.label, 1, wx.EXPAND | wx.ADJUST_MINSIZE)
self.slsizer = wx.BoxSizer(wx.HORIZONTAL)
self.slsizer.Add(self.line, 1, wx.EXPAND | wx.ADJUST_MINSIZE)
self.sizer = wx.BoxSizer(wx.VERTICAL)
self.sizer.Add(self.lsizer, 1, wx.EXPAND)
self.sizer.Add(self.slsizer, 0, wx.EXPAND)
self.SetAutoLayout(True)
self.SetSizer(self.sizer)
self.sizer.SetSizeHints(self)
self.sizer.Fit(self)
# Hide the progress bar initially. We don't event add it into the sizer, since it
# takes up space if we do
self.progress_bar.Show(False)
self.initializeThreadEventTable()
self.Bind(events.EVT_THREAD, self.onThreadEvent)
self.Bind(wx.EVT_RIGHT_DOWN, self.onRightClick)
self.image.Bind(wx.EVT_RIGHT_DOWN, self.onRightClick)
self.label.Bind(wx.EVT_RIGHT_DOWN, self.onRightClick)
def loadBitmaps(self):
"""Loads icon bitmaps into instance variables.
The bitmap displayed for the thread action may change many times over the course of
this widget's lifetime. This precaches the icons in the instance variables."""
self.idle_bitmap = icons.thread_idle.getBitmap()
self.connect_bitmap = icons.thread_connect.getBitmap()
self.listing_bitmap = icons.thread_listing.getBitmap()
self.abort_bitmap = icons.thread_abort.getBitmap()
self.action_bitmap = icons.thread_action.getBitmap()
self.transfer_bitmap = icons.thread_download.getBitmap()
def onRightClick(self, event):
"""Processes a right click event by displaying the action popup menu."""
menu = self.makePopupMenu()
self.PopupMenu(menu, event.GetPosition())
def makePopupMenu(self):
"""Creates the popup menu for the thread entry."""
menu = wx.Menu()
menu.Append(self.idCANCEL_TRANSFER, _("Cancel Transfer"),
_("Cancel this thread's transfer"))
menu.Append(self.idCANCEL_THREAD, _("Cancel Thread"),
_("Stops the curren thread"))
menu.AppendSeparator()
thread_entry = self.getEntryThread()
if thread_entry.isIdleEnabled():
menu.Append(self.idTOGGLE_TIMEOUT, _("Disable Idle Timeout"),
_("Disables timeout while idling"))
else:
menu.Append(self.idTOGGLE_TIMEOUT, _("Enable Idle Timeout"),
_("Enables timeout while idling"))
self.Bind(wx.EVT_MENU, self.onCancelTransfer, id=self.idCANCEL_TRANSFER)
self.Bind(wx.EVT_MENU, self.onCancelThread, id=self.idCANCEL_THREAD)
self.Bind(wx.EVT_MENU, self.onToggleTimeout, id=self.idTOGGLE_TIMEOUT)
return menu
def updateVisualDisplay(self):
"""Updates the layout of the widget after a content change."""
self.sizer.SetSizeHints(self)
parent = self.GetParent()
parent.Freeze()
parent.Layout()
parent.Thaw()
def getId(self):
"""Returns the ID for the underlying connection thread."""
return self.thread_id
def getEntryThread(self):
"""Returns the thread object for the underlying connection thread."""
thread_mgr = utils.getAppThreadManager()
return thread_mgr.getThread(self.thread_id)
def getHost(self):
"""Returns the host associated with the thread entry."""
entry = self.getEntryThread()
return entry.getHost()
def getPort(self):
"""Returns the port associated with the thread entry."""
entry = self.getEntryThread()
return entry.getPort()
def getTransport(self):
"""Returns the transport constant associated with the thread entry."""
entry = self.getEntryThread()
return entry.getTransport()
def Destroy(self):
"""Destroys the widget and the associated connection thread."""
if __debug__:
print "Destroying thread with ID: %s" %self.thread_id
self.thread.destroyConnection()
thread_mgr = utils.getAppThreadManager()
thread_mgr.removeThread(self.getId())
wx.Panel.Destroy(self)
def onCancelTransfer(self, events):
"""Cancels a currently executing transfer."""
self.label.SetLabel(self.getThreadName() +
_("Host: %(host)s\nCancelling Transfer...\n") %{ 'host' : self.getHost() })
self.image.SetBitmap(self.abort_bitmap)
self.updateVisualDisplay()
self.thread.abort()
def onCancelThread(self, event):
"""Cancels the current thread."""
if self.busy():
self.onCancelTransfer(event)
self.thread.setDone()
def onToggleTimeout(self, event=None):
"""Toggles the idling timeout."""
flag = self.thread.isIdleEnabled()
toggle = bool((flag + 1) % 2)
self.thread.setIdleEnabled(toggle)
def getThreadName(self):
"""Returns the name of the current thread.
If the thread does not have a name, then an empty string is returned. Otherwise,
the name returned has a trailing space to simplify concatenation."""
name = self.thread.getName()
if name:
return "[%s] " %name
return ''
def busy(self):
"""Returns a boolean indicating that the associated connection thread is busy."""
return self.thread.busy()
def finished(self):
"""Returns a boolean indicating that the associated connection thread is done."""
return self.thread.finished()
def isTransfering(self):
"""Returns a boolean indicating whether the associated connection thread is
executing a transfer."""
return self.thread.isTransfering()
def initializeThreadEventTable(self):
"""Creates a table of thread event kinds to widget update methods that reflect
thread activity."""
self.thread_event_table = {
events.ThreadEvent.EVT_CONNECT : self.updateConnect,
events.ThreadEvent.EVT_LIST : self.updateList,
events.ThreadEvent.EVT_CWD : self.updateCwd,
events.ThreadEvent.EVT_RENAME : self.updateRename,
events.ThreadEvent.EVT_DELETE : self.updateDelete,
events.ThreadEvent.EVT_MKDIR : self.updateMkdir,
events.ThreadEvent.EVT_RMDIR : self.updateRmdir,
events.ThreadEvent.EVT_CHMOD : self.updateChmod,
}
def onThreadEvent(self, event):
"""Processes a thread event by updating the display to reflect the thread's
activities."""
try:
evt_method = self.thread_event_table[event.kind]
evt_method(event)
except KeyError:
if __debug__:
print "Thread [%d] received unkown event of type: [%d]" \
%(self.getId(), event.kind)
def updateThreadEvent(self, label_text, bitmap):
"""Performs a generic update action for a thread event.
This sets the label text and bitmap accordingly and updates the visual display."""
self.label.SetLabel(label_text)
self.image.SetBitmap(bitmap)
self.updateVisualDisplay()
def updateConnect(self, event):
"""Updates the display to reflect an initiating connection."""
if __debug__:
print "Thread [%d] received event: [CONNECT]" %self.getId()
txt = self.getThreadName() + _("Host: %(host)s\nConnecting...") \
%{ 'host' : self.getHost() }
self.updateThreadEvent(txt, self.connect_bitmap)
def updateList(self, event):
"""Updates the display to reflect getting a listing."""
if __debug__:
print "Thread [%d] received event: [LIST]" %self.getId()
txt = self.getThreadName() + _("Host: %(host)s\nRetrieving Directory Listing.") \
%{ 'host' : self.getHost() }
self.updateThreadEvent(txt, self.listing_bitmap)
def updateCwd(self, event):
"""Updates the display to reflect a change in directory."""
if __debug__:
print "Thread [%d] received event: [CWD]" %self.getId()
txt = self.getThreadName() + _("Host: %(host)s\nChanging Directory...") \
%{ 'host' : self.getHost() }
self.updateThreadEvent(txt, self.action_bitmap)
def updateRename(self, event):
"""Updates the display to reflect a renaming event."""
if __debug__:
print "Thread [%d] received event: [RENAME]" %self.getId()
old, new = (event.data[0], event.data[1])
txt = self.getThreadName() + _("Host: %(host)s\nRenaming %(old)s to %(new)s...") \
%{ 'host' : self.getHost(), 'old' : old, 'new' : new }
self.updateThreadEvent(txt, self.action_bitmap)
def updateDelete(self, event):
"""Updates the display to reflect a delete event."""
if __debug__:
print "Thread [%d] received event: [DELETE]" %self.getId()
txt = self.getThreadName() + _("Host: %(host)s\nDeleting File...") \
%{ 'host' : self.getHost() }
self.updateThreadEvent(txt, self.action_bitmap)
def updateMkdir(self, event):
"""Updates the display to reflect the creation of a new directory."""
if __debug__:
print "Thread [%d] received event: [MKDIR]" %self.getId()
txt = self.getThreadName() + _("Host: %(host)s\nCreating Directory...") \
%{ 'host' : self.getHost() }
self.updateThreadEvent(txt, self.action_bitmap)
def updateRmdir(self, event):
"""Updates the display to reflect the removal of a directory."""
if __debug__:
print "Thread [%d] received event: [RMDIR]" %self.getId()
txt = self.getThreadName() + _("Host: %(host)s\nRemoving Directory...") \
%{ 'host' : self.getHost() }
self.updateThreadEvent(txt, self.action_bitmap)
def updateChmod(self, event):
"""Updates the display to reflect a change of permissions."""
txt = self.getThreadName() + _("Host: %(host)s\nApplying Permissions...") \
%{ 'host' : self.getHost() }
self.updateThreadEvent(txt, self.action_bitmap)
def updateIdle(self):
"""Updates the idle time and display."""
if self.thread.isIdleEnabled():
self.thread.updateIdleTime()
self.hideProgressBar()
txt = self.getThreadName() + _("Host: %(host)s\nIdle for %(time)s secs") \
%{ 'host' : self.getHost(), 'time' : self.thread.getIdleTime() }
self.updateThreadEvent(txt, self.idle_bitmap)
def displayProgressBar(self):
"""Shows the progress bar within the thread entry widget."""
if not self.progress_bar.IsShown():
self.progress_bar.Show(True)
self.sizer.Prepend(self.psizer, 0, wx.EXPAND)
self.sizer.Layout()
self.updateVisualDisplay()
wx.Yield()
def hideProgressBar(self):
"""Hides the progress bar from view."""
if self.progress_bar.IsShown():
self.progress_bar.Show(False)
# Work-around for removing the sizer since the normal Remove() tries to delete
# the sizer upon removal
children = [ x.GetSizer() for x in self.sizer.GetChildren() ]
pos = children.index(self.psizer)
item = self.sizer.GetChildren()[pos]
if item.IsSizer():
item.SetSizer(None)
self.sizer.Remove(pos)
self.sizer.Layout()
self.updateVisualDisplay()
wx.Yield()
def updateTransferEstimate(self):
"""Updates the estimated progress and speed of a transfer."""
rate = self.thread.getTransferSpeed()
cur, max = self.thread.getProgress()
if not self.progress_bar.IsShown():
self.progress_bar.SetRange(max)
self.displayProgressBar()
self.progress_bar.SetValue(cur)
if rate:
remaining = long(float(max - cur) / (rate * 1024.0))
else:
remaining = 0
hrs = remaining / 3600
min = (remaining % 3600) / 60
sec = remaining % 60
event = self.thread.loadTransferState()
txt = ''
if event.direction == protocol.ProtocolInterface.DOWNLOAD:
txt = self.getThreadName() + \
_("Host: %(host)s\nDownloading %(file)s with %(rate).02f kb/s, %(cur)d of %(end)d bytes, %(hrs)02d:%(min)02d:%(sec)02d sec left") \
%{ 'host' : event.host, 'file' : event.file, 'rate' : rate, 'cur' : cur,
'end' : max, 'hrs' : hrs, 'min' : min, 'sec' : sec }
elif event.direction == protocol.ProtocolInterface.UPLOAD:
txt = self.getThreadName() + \
_("Host: %(host)s\nUploading %(file)s with %(rate).02f kb/s, %(cur)d of %(end)d bytes, %(hrs)02d:%(min)02d:%(sec)02d sec left") \
%{ 'host' : event.host, 'file' : event.file, 'rate' : rate, 'cur' : cur,
'end' : max, 'hrs' : hrs, 'min' : min, 'sec' : sec }
self.updateThreadEvent(txt, self.transfer_bitmap)
class DownloadTab(wx.Panel):
"""Download tab.
This tab displays a list of completed downloads. It also includes the archive window,
which allows for expanding of archives within the transfer window. Archives can be
expanded to a specified directory, provided that the archive type is supported."""
idTOOL_CLEAR = wx.NewId()
idTOOL_EXPLORE = wx.NewId()
idATTEMPTS = wx.NewId()
def __init__(self, parent):
"""Creates the transfer tab."""
wx.Panel.__init__(self, parent, -1)
self.status_headers = [ _("Filename"), _("Status"), _("Type"), _("Size") ]
self.archive_headers = [ _("Filename"), _("Size") ]
self.toolbar = self.makeToolBar()
# Create the status list
self.event_list = [ ]
self.status_list = wx.ListCtrl(self, -1,
style=wx.LC_REPORT | wx.SUNKEN_BORDER | wx.LC_SINGLE_SEL)
for col, name in zip(range(len(self.status_headers)), self.status_headers):
self.status_list.InsertColumn(col, name)
self.status_list.SetColumnWidth(0, 125)
# Create the archive window
self.arch_list = [ ]
self.archive_list = wx.ListCtrl(self, -1, style=wx.LC_REPORT | wx.SUNKEN_BORDER)
for col, name in zip(range(len(self.archive_headers)), self.archive_headers):
self.archive_list.InsertColumn(col, name)
self.archive_list.SetColumnWidth(0, 200)
# Set up master sizer
sizer = wx.BoxSizer(wx.VERTICAL)
sizer.Add(self.toolbar, 0, wx.EXPAND)
sizer.Add((0, 2))
sizer.Add(self.status_list, 1, wx.EXPAND)
sizer.Add(self.archive_list, 0, wx.EXPAND)
sizer.SetItemMinSize(self.archive_list, -1, 200)
self.SetAutoLayout(True)
self.SetSizer(sizer)
self.Bind(events.EVT_TRANSFER, self.onTransferEvent)
evt_registry = events.getEventRegistry()
evt_registry.registerEventListener(events.EVT_TRANSFER_TYPE, self)
# Create an archiver instance
self.archiver = archiver.Archiver(self)
def makeToolBar(self):
"""Makes the download tab toolbar."""
toolbar = wx.ToolBar(self, -1)
toolbar.SetToolBitmapSize(wx.Size(20, 20))
bitmap = icons.clear.getBitmap()
toolbar.AddTool(self.idTOOL_CLEAR, bitmap, shortHelpString=_("Clear List"))
bitmap = icons.check_archive.getBitmap()
toolbar.AddTool(self.idTOOL_EXPLORE, bitmap, shortHelpString=_("Explore Archive"))
toolbar.AddSeparator()
config = utils.getApplicationConfiguration()
self.max_attempts = wx.SpinCtrl(toolbar, self.idATTEMPTS, min=1, max=16,
size=wx.Size(50, -1))
if config.has_key('retries'):
self.max_attempts.SetValue(config['retries'])
toolbar.AddControl(self.max_attempts)
self.Bind(wx.EVT_SPINCTRL, self.onSpinUpdate, id=self.idATTEMPTS)
toolbar.AddSeparator()
label = wx.StaticText(toolbar, -1, _("max attempts"))
toolbar.AddControl(label)
self.Bind(wx.EVT_TOOL, self.onClear, id=self.idTOOL_CLEAR)
self.Bind(wx.EVT_TOOL, self.onExplore, id=self.idTOOL_EXPLORE)
toolbar.Realize()
return toolbar
def onSpinUpdate(self, event):
"""Processes an update to the number of download retries."""
value = self.max_attempts.GetValue()
config = utils.getApplicationConfiguration()
config['retries'] = value
def onTransferEvent(self, event):
"""Processes a transfer event.
This widget only cares about download events. Upload events are discarded."""
if event.direction != protocol.ProtocolInterface.DOWNLOAD:
return
path = os.path.join(event.local_path, event.file)
try:
size = os.path.getsize(path)
size = utils.beautifySize(size)
except OSError, strerror:
size = _("Err")
if self.archiver.findSupportedHandler(path) is not None:
status = self.archiver.getStatus(path)
type = self.archiver.getType(path)
else:
status = _("Ok")
type = _("Unknown")
self.addStatusListItem(event.file, status, type, size)
self.event_list.append(event)
def addStatusListItem(self, file, status, type, size_str):
"""Adds a download item to the download list."""
index = self.status_list.GetItemCount()
self.status_list.InsertStringItem(index, file)
self.status_list.SetStringItem(index, 1, status)
self.status_list.SetStringItem(index, 2, type)
self.status_list.SetStringItem(index, 3, size_str)
def addArchiveListItem(self, file, size):
"""Adds a file to the archive file list."""
index = self.archive_list.GetItemCount()
self.archive_list.InsertStringItem(index, file)
self.archive_list.SetStringItem(index, 1, str(size))
def onClear(self, event):
"""Clears the status and archive lists."""
self.clearStatusList()
self.clearArchiveList()
def clearStatusList(self):
"""Clears the download status list."""
self.status_list.Freeze()
self.status_list.DeleteAllItems()
self.event_list = [ ]
self.status_list.Thaw()
def clearArchiveList(self):
"""Clears the archive file list."""
self.archive_list.Freeze()
self.archive_list.DeleteAllItems()
self.arch_list = [ ]
self.archive_list.Thaw()
def onExplore(self, event):
"""Unpacks an archive and displays the contents in the archive window."""
cleared = False
selected = utils.getSelected(self.status_list)
for x in selected:
item = self.event_list[x]
unpack_loc = messages.displayDirDialog(self,
_("Ftpcube - Select Directory to Unpack Archive: %(arch)s")
%{ 'arch' : item.file })
if unpack_loc:
if not cleared:
cleared = True
self.clearArchiveList()
try:
files = self.archiver.unpack(os.path.join(item.local_path, item.file),
unpack_loc)
except Exception, strerror:
messages.displayErrorDialog(self, _("Error unpacking archive: %(err)s")
%{ 'err' : strerror })
continue
if files is None:
messages.displayErrorDialog(self, _("Invalid archive: %(f)s")
%{ 'f' : item.file })
continue
loc_len = len(unpack_loc) + 1
for f in files:
try:
size = os.path.getsize(f)
except OSError, strerror:
size = _("Unknown")
self.addArchiveListItem(f[loc_len:], size)
self.arch_list.append(f)
class FailureTab(wx.Panel):
"""Failure tab.
This tab holds entries whose transfers have exceeded the retry attempts and cannot be
completed. These entries can be resubmitted to the queue unpon selection. The cause is
displayed in the error message column if known."""
idTOOL_CLEAR = wx.NewId()
idTOOL_RESUBMIT = wx.NewId()
def __init__(self, parent):
"""Creates the failure tab."""
wx.Panel.__init__(self, parent, -1)
self.headers = [ _("Server"), _("Filename"), _("Size"), _("Error") ]
self.toolbar = self.makeToolBar()
self.failure_list = [ ]
self.list = wx.ListCtrl(self, -1, style=wx.LC_REPORT | wx.SUNKEN_BORDER)
for col, name in zip(range(len(self.headers)), self.headers):
self.list.InsertColumn(col, name)
self.list.SetColumnWidth(0, 80)
self.list.SetColumnWidth(1, 100)
self.list.SetColumnWidth(2, 80)
self.list.SetColumnWidth(3, 500)
# Set up the master sizer
sizer = wx.BoxSizer(wx.VERTICAL)
sizer.Add(self.toolbar, 0, wx.EXPAND)
sizer.Add((0, 2))
sizer.Add(self.list, 1, wx.EXPAND)
self.SetAutoLayout(True)
self.SetSizer(sizer)
self.Bind(events.EVT_FAILURE, self.onFailureEvent)
evt_registry = events.getEventRegistry()
evt_registry.registerEventListener(events.EVT_FAILURE_TYPE, self)
def makeToolBar(self):
"""Creates the failure tab toolbar."""
toolbar = wx.ToolBar(self, -1)
toolbar.SetToolBitmapSize(wx.Size(20, 20))
bitmap = icons.clear.getBitmap()
toolbar.AddTool(self.idTOOL_CLEAR, bitmap, shortHelpString=_("Clear List"))
bitmap = icons.resubmit_job.getBitmap()
toolbar.AddTool(self.idTOOL_RESUBMIT, bitmap, shortHelpString=_("Resubmit Job"))
self.Bind(wx.EVT_TOOL, self.onClear, id=self.idTOOL_CLEAR)
self.Bind(wx.EVT_TOOL, self.onResubmit, id=self.idTOOL_RESUBMIT)
toolbar.Realize()
return toolbar
def onFailureEvent(self, event):
"""Processes a failure event.
If the failure event has not yet reached the maximum number of attempts, it is
resubmitted into the queue. Otherwise, it is added to the failure list."""
event.attempt = event.attempt + 1
if event.attempt > event.max_attempts:
self.addListItem(event.host, event.file, event.size, event.error)
self.failure_list.append(event)
else:
queue_evt = events.EnqueueEvent(**event.toHash())
evt_registry = events.getEventRegistry()
evt_registry.postEvent(queue_evt)
def addListItem(self, server, file, size, err_msg):
"""Adds a failure entry into the failure list."""
if isinstance(size, int) or isinstance(size, long):
size = utils.beautifySize(size)
index = self.list.GetItemCount()
self.list.InsertStringItem(index, server)
self.list.SetStringItem(index, 1, file)
self.list.SetStringItem(index, 2, size)
self.list.SetStringItem(index, 3, str(err_msg))
def onClear(self, event):
"""Clears the entries in the failure tab."""
self.list.Freeze()
self.list.DeleteAllItems()
self.failure_list = [ ]
self.list.Thaw()
def onResubmit(self, event):
"""Processes an event to resubmit an item to the transfer queue."""
evt_registry = events.getEventRegistry()
selected = utils.getSelected(self.list)
for x in selected:
evt = self.failure_list[x]
evt.attempt = 0 # Reset attempts counter
new_event = events.EnqueueEvent(**evt.toHash())
evt_registry.postEvent(new_event)
# Now remote the item from the list
self.list.Freeze()
for x in selected:
self.failure_list.remove(self.failure_list[x])
self.list.DeleteItem(x)
self.list.Thaw()
|