ctrlwin.py :  » Network » FtpCube » ftpcube-0.5.1 » libftpcube » Python Open Source

Home
Python Open Source
1.3.1.2 Python
2.Ajax
3.Aspect Oriented
4.Blog
5.Build
6.Business Application
7.Chart Report
8.Content Management Systems
9.Cryptographic
10.Database
11.Development
12.Editor
13.Email
14.ERP
15.Game 2D 3D
16.GIS
17.GUI
18.IDE
19.Installer
20.IRC
21.Issue Tracker
22.Language Interface
23.Log
24.Math
25.Media Sound Audio
26.Mobile
27.Network
28.Parser
29.PDF
30.Project Management
31.RSS
32.Search
33.Security
34.Template Engines
35.Test
36.UML
37.USB Serial
38.Web Frameworks
39.Web Server
40.Web Services
41.Web Unit
42.Wiki
43.Windows
44.XML
Python Open Source » Network » FtpCube 
FtpCube » ftpcube 0.5.1 » libftpcube » ctrlwin.py
"""
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()
www.java2java.com | Contact Us
Copyright 2009 - 12 Demo Source and Support. All rights reserved.
All other trademarks are property of their respective owners.