Example #1
0
class MainWindow(QMainWindow, ui_cghubdownloader.Ui_CGHubDownloader):
	def __init__(self, parent=None):
		super(MainWindow, self).__init__(parent)
		
		#  Start logging
		#  ----------------------------------------
		logger = logging.getLogger("mainwindow.__init__")
		
		#  Member variables and init stuffs
		#  ----------------------------------------
		self.projdir = os.path.join(os.getcwd(), 'Downloads')
		
		if not os.path.isdir(self.projdir):
			os.makedirs(self.projdir)
		
		self.addedUsers = []
		self.currentuser = ''
		self.currentusermodel = None         # holds UserImagesModel
		self.currentthumbnailsmodel = None   # holds ThumbnailsModel
		self.currentview = 0   # 0 -> Table view, 1 -> Thumbnails view
		self.setupUi(self)
		
		#  Factory
		#  ----------------------------------------
		self.factory = Factory()
		self.factory.addClass('UserValidation', ProcessUserValidation)
		self.factory.addWorkers('UserValidation')
		self.factory.addClass('UserImages', ProcessUserImages)
		self.factory.addWorkers('UserImages')
		self.factory.addClass('ImagesFilesize', CheckImagesFilesize)
		self.factory.addWorkers('ImagesFilesize')
		
		for worker in self.factory.getWorkers('UserValidation'):
			self.connect(worker, SIGNAL('error'), self.callError)
			self.connect(worker, SIGNAL('updateList'), self.refreshUserList)
		
		for worker in self.factory.getWorkers('UserImages'):
			self.connect(worker, SIGNAL('error'), self.callError)
			self.connect(worker, SIGNAL('updateTable'), self.refreshTableView)
		
		for worker in self.factory.getWorkers('ImagesFilesize'):
			self.connect(worker, SIGNAL('updateTableModel'), self.updateTableModel)
		
		#  Users data
		#  ----------------------------------------
		self.usersdata = UsersData(self)
		self.usersdata.setProjectDir(self.projdir)
		self.usersdata.error.connect(self.callError) # SIGNAL("error()")
		self.userslistdata = UsersList(self.usersdata)
		self.userslistdata.setProjectDir(self.projdir)
		self.userslistdata.updateFromUsersData(self.usersdata)
		
		for worker in self.factory.getWorkers('UserImages'):
			self.connect(worker, SIGNAL('updateTable'), self.userslistdata.updateImagesCount)
		
		#  Project manager
		#  ----------------------------------------
		self.projman = ProjectManager(self.projdir)
		self.projman.updateWithExistingImages(self.usersdata)
		self.projman.saveData.connect(self.saveData) # SIGNAL("saveData()")
		
		#  Users list
		#  ----------------------------------------
		self.userslistmodel = UsersListModel(self.userslistdata)
		self.users_listViewWidget.setModel(self.userslistmodel)
		self.users_listViewWidget.setGridSize(QSize(20,20))
		headerstyle = "font-size: 12px;"
		self.users_listViewWidget.setStyleSheet(headerstyle)
		# Handle selection changed
		selectionmodel = self.users_listViewWidget.selectionModel()
		selectionmodel.selectionChanged.connect(self.userslistmodel.handleSelectionChanged)
		self.userslistmodel.onSelectionChanged.connect(self.onUserSelect)
		
		#  Setup layout inside stacked widget
		#  ----------------------------------------
		#  ( I can't understand how to set this up in Qt Designer,
		#    I can only create layout for one of them, then there
		#    seems to be no way to set layout for the other one )
		self.horizontalLayout_a = QHBoxLayout(self.page)
		self.horizontalLayout_a.addWidget(self.images_tableViewWidget)
		self.horizontalLayout_b = QHBoxLayout(self.page_2)
		self.horizontalLayout_b.addWidget(self.thumbnails_tableViewWidget)
		
		#  Table View
		#  ----------------------------------------
		self.gettablemodel = {}
		self.setTableModel('')
		#  Set table header's font size
		headerstyle = "QHeaderView { font-family: Segoe UI Light; font-size: 12px; }"
		self.images_tableViewWidget.horizontalHeader().setStyleSheet(headerstyle)
		self.images_tableViewWidget.verticalHeader().setStyleSheet(headerstyle)
		#  Set column width
		self.images_tableViewWidget.setColumnWidth(2,100)
		self.images_tableViewWidget.setColumnWidth(1,100)
		self.images_tableViewWidget.horizontalHeader().setResizeMode(1, QHeaderView.Fixed)
		self.images_tableViewWidget.horizontalHeader().setResizeMode(2, QHeaderView.Fixed)
		
		#  Thumbnails View
		#  ----------------------------------------
		self.getthumbnailsmodel = {}
		self.setThumbnailsModel('')
		self.thumbnails_tableViewWidget.setIconSize(QSize(64, 64))
		
		#  Table view popup menu
		#  ----------------------------------------
		self.table_popMenu = QMenu(self)
		checkSelected_Action = QAction('Check Selected', self)
		uncheckSelected_Action = QAction('Uncheck Selected', self)
		checkAll_Action = QAction('Check All', self)
		uncheckAll_Action = QAction('Uncheck All', self)
		viewFile_Action = QAction('View File', self)
		self.connect(checkAll_Action, SIGNAL("triggered()"), partial(self.checkSelected, True))
		self.connect(uncheckAll_Action, SIGNAL("triggered()"), partial(self.checkSelected, False))
		self.connect(checkAll_Action, SIGNAL("triggered()"), partial(self.checkAll, True))
		self.connect(uncheckAll_Action, SIGNAL("triggered()"), partial(self.checkAll, False))
		self.connect(viewFile_Action, SIGNAL("triggered()"), self.viewFile)
		self.table_popMenu.addAction(checkSelected_Action)
		self.table_popMenu.addAction(uncheckSelected_Action)
		self.table_popMenu.addSeparator()
		self.table_popMenu.addAction(checkAll_Action)
		self.table_popMenu.addAction(uncheckAll_Action)
		self.table_popMenu.addSeparator()
		self.table_popMenu.addAction(viewFile_Action)
		self.images_tableViewWidget.setContextMenuPolicy(Qt.CustomContextMenu)
		self.images_tableViewWidget.customContextMenuRequested.connect(self.tableviewPopup)
		
		#  Thumbnails view popup menu
		#  ----------------------------------------
		self.thumbnails_popMenu = QMenu(self)
		thumbCheckSelected_Action = QAction('Check Selected', self)
		thumbUncheckSelected_Action = QAction('Uncheck Selected', self)
		thumbCheckAll_Action = QAction('Check All', self)
		thumbUncheckAll_Action = QAction('Uncheck All', self)
		self.connect(thumbCheckSelected_Action, SIGNAL("triggered()"), partial(self.checkSelected, True))
		self.connect(thumbUncheckSelected_Action, SIGNAL("triggered()"), partial(self.checkSelected, False))
		self.connect(thumbCheckAll_Action, SIGNAL("triggered()"), partial(self.checkAll, True))
		self.connect(thumbUncheckAll_Action, SIGNAL("triggered()"), partial(self.checkAll, False))
		self.thumbnails_popMenu.addAction(thumbCheckSelected_Action)
		self.thumbnails_popMenu.addAction(thumbUncheckSelected_Action)
		self.thumbnails_popMenu.addSeparator()
		self.thumbnails_popMenu.addAction(thumbCheckAll_Action)
		self.thumbnails_popMenu.addAction(thumbUncheckAll_Action)
		self.thumbnails_tableViewWidget.setContextMenuPolicy(Qt.CustomContextMenu)
		self.thumbnails_tableViewWidget.customContextMenuRequested.connect(self.thumbnailsViewPopup)
		
		#  Users list view popup menu
		#  ----------------------------------------
		self.list_popMenu = QMenu(self)
		deleteUser_Action = QAction('Delete user', self)
		self.connect(deleteUser_Action, SIGNAL("triggered()"), self.deleteUser)
		self.list_popMenu.addAction(deleteUser_Action)
		self.users_listViewWidget.setContextMenuPolicy(Qt.CustomContextMenu)
		self.users_listViewWidget.customContextMenuRequested.connect(self.listviewPopup)
		
		#  Set project button popup menu
		#  ----------------------------------------
		self.setproject_buttonWidget.setContextMenuPolicy(Qt.CustomContextMenu)
		self.setproject_buttonWidget.customContextMenuRequested.connect(self.projectButtonPopup)
		self.project_popMenu = QMenu(self)
		openProjectDir_Action = QAction('Open Download location', self)
		self.connect(openProjectDir_Action, SIGNAL("triggered()"), partial(self.openFolder, 'project'))
		self.project_popMenu.addAction(openProjectDir_Action)
		
		#  Avatar
		#  ----------------------------------------
		self.icon = QIcon()
		#  Set default avatar
		self.loadAvatar(":no_avatar100.gif")
		self.username_labelWidget.setText(self.currentuser)
		
		#  Log Dock Widget
		#  ----------------------------------------
		logDockWidget = QDockWidget("Log", self)
		logDockWidget.setObjectName("LogDockWidget")
		logDockWidget.setAllowedAreas(Qt.LeftDockWidgetArea | Qt.RightDockWidgetArea)
		self.logtext = LogText()
		self.log_TextBrowser = self.logtext.textbrowser
		logDockWidget.setWidget(self.log_TextBrowser)
		self.addDockWidget(Qt.RightDockWidgetArea, logDockWidget)
		self.log_TextBrowser.setStyleSheet(stylesheets.logArea)
		logDockWidget.hide()
		
		#  Menu Additions
		#  ----------------------------------------
		#  View menu
		viewDatabase_Action = self.createAction('View Database', self.openDatViewer, "Alt+D", None, None, False)
		switchView_Action = self.createAction('Switch view', self.userSwitchView, "Alt+Q", None, None, False)
		# self.menu_View.addAction(logDockWidget.toggleViewAction())
		self.view_Menu.addAction(switchView_Action)
		self.view_Menu.addSeparator()
		self.view_Menu.addAction(viewDatabase_Action)
		#  Help menu
		about_Action = self.createAction('About', self.showAboutDialog, "", None, None, False)
		# self.about_Action.activated.connect(self.showAboutDialog)
		self.help_Menu.addAction(about_Action)
		
		#  Connect Signals and Slots
		#  ----------------------------------------
		#  Main Widgets
		self.adduser_buttonWidget.pressed.connect(self.onAddUser)			# SIGNAL("pressed()")
		self.newuser_lineEditWidget.returnPressed.connect(self.onAddUser)	# SIGNAL("returnPressed()")
		self.users_listViewWidget.doubleClicked.connect(self.onUserSelect)
		
		# Note: I leave out activated signal because I not sure what it does
		# self.users_listViewWidget.activated.connect(self.onUserSelect)
		
		# Note: clicked signal is not used because we already have
		#  self.userslistmodel.onSelectionChanged.connect(self.onUserSelect).
		#  Adding both will double trigger the onUserSelect method which
		#  will cause the view be refresed twice
		# self.users_listViewWidget.clicked.connect(self.onUserSelect)
		
		#  Buttons on left panel
		self.setproject_buttonWidget.clicked.connect(self.setProjectLocation)
		self.download_buttonWidget.clicked.connect(self.downloadSelected)
		self.avatar_buttonWidget.clicked.connect(partial(self.openFolder, 'user'))
		self.tableview_buttonWidget.clicked.connect(partial(self.switchView, 0))
		self.thumbnailview_buttonWidget.clicked.connect(partial(self.switchView, 1))
		#  Actions
		self.test_action.triggered.connect(self.test)
		self.openDatViewer_action.triggered.connect(self.openDatViewer)
				
		#  Finalizing
		#  ----------------------------------------
		self.newuser_lineEditWidget.setFocus()
		self.resize(900,500)
		self.setStatus("Hello", 1.5)
		self.switchView(1)   # Set view to table view
		self.setProjectDir(os.path.join(os.getcwd(), 'Downloads'))
		QTimer.singleShot(0, self.newuser_lineEditWidget, SLOT('setFocus()'))
		
		logger.info("Finish constructing main window")
	
	def createAction( self, text, slot=None, shortcut=None, icon=None, tip=None, checkable=False, signal="triggered()" ):
		# ( Copied from Rapid GUI Programming with Python and Qt,
		#		Chapter 6, imagechanger.py )
		action = QAction( text, self )
		if icon is not None:
			action.setIcon( QIcon(":/%s.png" % icon) )
		if shortcut is not None:
			action.setShortcut( shortcut )
		if tip is not None:
			action.setToolTip( tip )
			action.setStatusTip( tip )
		if slot is not None:
			self.connect( action, SIGNAL(signal), slot )
		if checkable:
			action.setCheckable( True )
		return action
	
	#  Non-UI methods
	#  ----------------------------------------
	def test(self):
		pass

	def logCurrentMethod(self, msg):
		methodname = inspect.stack()[1][3]
		logger = logging.getLogger("mainwindow."+methodname)
		logger.info(msg)

	def printToLog(self, msg=''):
		self.logtext.addNewMsg(msg)	
	
	def callWarning(self, msg=''):
		self.logtext.addNewMsg(msg)
		QMessageBox.warning(self, 'Warning', msg)

	def callError(self, msg=''):
		self.logtext.addNewMsg(msg)
		QMessageBox.warning(self, 'Error', msg)
	
	def setStatus(self, msg, secs=3):
		self.statusbar.showMessage(msg, secs*1000)
	
	def saveData(self):
		self.logCurrentMethod("Saving self.usersdata")
		self.usersdata.savePickle()
	
	def loadAvatar(self, avatarpath):
		self.logCurrentMethod("Loading avatar: {0}".format(avatarpath))
		self.icon.addPixmap(QPixmap(avatarpath), QIcon.Normal, QIcon.Off)
		self.avatar_buttonWidget.setIcon(self.icon)
		self.avatar_buttonWidget.setIconSize(QSize(50, 50))
	
	def setProjectDir(self, location):
		self.logCurrentMethod("Setting project dir to: {0}".format(location))
		self.projman.setProjectDir(location)
		self.setWindowTitle('CG Hub Downloader - {0}'.format(location))
	
	def switchView(self, view):
		self.logCurrentMethod("Switching view to...")
		button = None
		
		if view == 0:
			self.logCurrentMethod("Table view")
			self.tableview_buttonWidget.setEnabled(False)
			self.thumbnailview_buttonWidget.setEnabled(True)
		else:
			self.logCurrentMethod("Thumbnails view")
			self.tableview_buttonWidget.setEnabled(True)
			self.thumbnailview_buttonWidget.setEnabled(False)
		
		self.currentview = view
		self.stackedWidget.setCurrentIndex(view)
	
	def userSwitchView(self):
		self.switchView(not self.currentview)
	
	#  Direct-UI methods
	#  ----------------------------------------
	def setProjectLocation(self):
		dialog = QFileDialog()
		dialog.setFileMode(QFileDialog.Directory)
		dialogdir = self.projdir # or can use QDir.currentPath()
		directory = dialog.getExistingDirectory(self, "Set download location", \
			dialogdir, QFileDialog.ShowDirsOnly)
		
		if directory:
			self.setProjectDir(directory)
			self.usersdata.setProjectDir(directory)
			self.userslistdata.setProjectDir(directory)
			self.userslistdata.updateFromUsersData(self.usersdata)
			
			# Deselect all from users list
			for modelindex in self.users_listViewWidget.selectedIndexes():
				selectionmodel = self.users_listViewWidget.selectionModel()
				selectionmodel.select(modelindex, QItemSelectionModel.Deselect)
			
			# Clear table view
			self.clearTable()
			
			# Load default avatar
			self.loadAvatar(":no_avatar100.gif")
		
	def downloadSelected(self):
		if self.currentuser != None and self.currentuser != '':
			self.logCurrentMethod("Downloading selected images from {0}".format(self.currentuser))
			userobj = self.usersdata.getUser(self.currentuser)
			self.projman.processUser(userobj)
	
	def openFolder(self, folderType):
		self.logCurrentMethod("Opening folder")
		if folderType == 'project':
			call(["explorer.exe ", convertPathToExplorer(self.projdir)])
		elif folderType == 'user':
			if self.currentuser != None and self.currentuser != '':
				userdir = convertPathToExplorer(self.projdir+'\\'+self.currentuser)
				if os.path.isdir(userdir):
					call(["explorer.exe ", userdir])
	
	def openDatViewer( self ):
		window = datviewer2.DatViewerWindow( self )
		window.show()
	
	def showAboutDialog(self):
		aboutdialog = AboutDialog(self)
		aboutdialog.show()

	#  Table View related methods
	#  ----------------------------------------
	def onUserSelect(self, modelindex):
		self.logCurrentMethod("Loading user's images")
		username = self.userslistmodel.data(modelindex, Qt.DisplayRole)
		# Note: username will looks like -> "username (10)"
		username = username.split(' ')[0]
		userobj = self.usersdata.getUser(username)
		self.factory.addJob('UserImages', (userobj, self.projman))

	def refreshTableView(self, userobj):
		self.logCurrentMethod("Updating table view")
		self.projman.updateWithExistingImages(self.usersdata)
		
		# It might always be true, but just to be clear, please be
		# aware that both table model and thumbnails model need to
		# receive the same userobj in order to sync with each other.
		self.setTableModel(userobj.name)
		self.setThumbnailsModel(userobj.name)
		self.currentthumbnailsmodel.updateModel()

		# Load avatar
		self.loadAvatar(userobj.avatarpath)
		
		# Set current user
		self.currentuser = userobj.name
		
		# Update user name
		self.username_labelWidget.setText(self.currentuser)
		
		# Update Images filesize
		self.updateImagesFilesize()
		
		# Update/Download Thumbnails
		self.projman.processUser(userobj, thumbnails=True)
		
		# Redownload avatar if it's missing
		if userobj.avatarpath == None:
			avatarpath = self.projman.getUserAvatar(userobj, userobj.userpagehtml)
			if avatarpath:
				self.loadAvatar(avatarpath)

	def setTableModel(self, username):
		""" Attact a table model to view. If no table model found for
		the user, create a new one.
		"""
		self.logCurrentMethod("Setting table model for '{0}'".format(username))
		model = None
		
		if username not in self.gettablemodel.keys():
			model = UserImagesModel(self.usersdata, username)
			self.gettablemodel[username] = model
			
			# When projman is downloading an image, it will fires
			# 'updateModel' signal. This model will catch it and 
			# calls 'updateModel' (which is just a simple 'modelReset()')
			# to tell the view to update. This process shows the
			# download progress
			self.projman.updateModel.connect(model.updateModel)
			
		else:
			model = self.gettablemodel[username]
		
		self.images_tableViewWidget.setModel(model)
			
		# Keep self updated about the change
		self.currentusermodel = model
	
	def updateTableModel(self):
		self.logCurrentMethod("Asking table model to 'resetModel()'")
		self.currentusermodel.updateModel()
	
	def clearTable(self):
		self.logCurrentMethod("Clearing table, setting table model to empty user ('')")
		self.images_tableViewWidget.clearSpans()
		self.setTableModel('')   # Set to empty user will clear the table model

	def updateImagesFilesize(self):
		if self.currentuser != None and self.currentuser != '':
			self.logCurrentMethod("Updating images file size for '{0}'".format(self.currentuser))
			self.projman.updateWithExistingImages(self.currentusermodel._usersdata)
			self.setStatus("Checking {0}'s images file size...".format(self.currentuser), 3.5)
			self.factory.addJob('ImagesFilesize', (self.usersdata, self.currentuser))
	
	#  Thumbnails view methods
	#  ----------------------------------------
	def setThumbnailsModel(self, username):
		""" Attacth a thumbnails model to view. If no thumbnails model found for
		the user, create a new one.
		"""
		self.logCurrentMethod("Setting thumbnails model for '{0}'".format(username))
		model = None
		
		if username not in self.getthumbnailsmodel.keys():
			# Passing a UserDir to ThumbnailsModel() so that it
			# knows where to look for the thumbnails
			userdir = self.projman.getUserDir(username)
			model = ThumbnailsModel(self.usersdata, username, userdir)
			self.getthumbnailsmodel[username] = model
			
			# The thumbnails table view is an instance of MyThumbnailsView,
			# MyThumbnailsView will fires the 'columnCountChange' everytime
			# the window is resized. This will tell the model to change
			# it's column counts to fit the new size.
			self.thumbnails_tableViewWidget.columnCountChange.connect(model.setColumnCount)
			
			# When projman is downloading a thumbnails, it will fires
			# 'updateThumbnailsModel' signal. This model will catch it and 
			# calls 'updateModel' (which is just a simple 'modelReset()')
			# to tell the view to update.
			self.projman.updateThumbnailsModel.connect(model.updateModel)
			
		else:
			model = self.getthumbnailsmodel[username]

		self.thumbnails_tableViewWidget.setModel(model)
			
		# Keep self updated about the change
		self.currentthumbnailsmodel = model

	#  Table view right click context menu
	#  ----------------------------------------
	def tableviewPopup(self, point):
		self.table_popMenu.exec_(self.images_tableViewWidget.mapToGlobal(point))

	def checkSelected(self, check):
		self.logCurrentMethod("Check selected")
		view = None
		model = None
		checkstate = Qt.Checked if check else Qt.Unchecked
		
		if self.stackedWidget.currentIndex() == 0:
			view = self.images_tableViewWidget
			model = self.currentusermodel
		else:
			view = self.thumbnails_tableViewWidget
			model = self.currentthumbnailsmodel
		
		for idx in view.selectedIndexes():
			model.setData(idx, checkstate, Qt.CheckStateRole)
	
	def checkAll(self, check):
		self.logCurrentMethod("Check all")
		view = None
		model = None
		checkstate = Qt.Checked if check else Qt.Unchecked
		
		if self.stackedWidget.currentIndex() == 0:
			view = self.images_tableViewWidget
			model = self.currentusermodel
		else:
			view = self.thumbnails_tableViewWidget
			model = self.currentthumbnailsmodel
		
		view.selectAll()
		for idx in view.selectedIndexes():
			model.setData(idx, checkstate, Qt.CheckStateRole)
		view.selectionModel().clearSelection()
	
	def viewFile(self):
		# for idx in self.images_tableViewWidget.selectedIndexes():
		# 	self.currentusermodel.setData(idx, Qt.Unchecked, Qt.CheckStateRole)
		pass

	#  Thumbnails view right click context menu
	#  ----------------------------------------
	def thumbnailsViewPopup(self, point):
		self.thumbnails_popMenu.exec_(self.thumbnails_tableViewWidget.mapToGlobal(point))

	#  Users list view right click context menu
	#  ----------------------------------------
	def listviewPopup(self, point):
		self.list_popMenu.exec_(self.users_listViewWidget.mapToGlobal(point))
	
	def deleteUser(self):
		# Delete from users list
		self.userslistdata.deleteUser(self.currentuser)
		# Delete from usersdata as well
		self.usersdata.deleteUser(self.currentuser)
		self.userslistmodel.updateModel()
		
		# Selection the first item on the list
		modelindex = self.userslistmodel.index(0,0)
		if modelindex:
			self.users_listViewWidget.setCurrentIndex(modelindex)
			username = self.userslistmodel.data(modelindex, Qt.DisplayRole)
			self.refreshTableView(self.usersdata.getUser(username))
	
	#  Set project button right click context menu
	#  -------------------------------------------
	def projectButtonPopup(self, point):
		self.project_popMenu.exec_(self.setproject_buttonWidget.mapToGlobal(point))

	#  User lists related methods
	#  ----------------------------------------
	def onAddUser( self ):
		user = self.newuser_lineEditWidget.text()
		user = str(user).lower()
		self.logCurrentMethod("Adding '{0}' to list".format(user))
		if len( user ) > 0:
			if user not in self.addedUsers:
				self.usersdata.addNewUser(user)
				userobj = self.usersdata.getUser(user)
				self.setStatus("Adding '{0}'...".format(user), 3)
				self.factory.addJob('UserValidation', userobj)
	
	def refreshUserList(self, userobj, html):
		self.logCurrentMethod("Refreshing users list")
		if userobj.exists and userobj.viewImageUrl == False:
			# need to mark user empty???
			self.callWarning("{0}'s gallery is empty".format(userobj.name))
			logger.warning("{0}'s gallery is empty".format(userobj.name))
		else:
			self.userslistdata.addUser(userobj)
			self.userslistmodel.updateModel()
			# Select new user
			idx = self.userslistdata.getUserIndex(userobj.name)
			if idx != None:
				modelindex = self.userslistmodel.index(idx, 0)
				self.users_listViewWidget.setCurrentIndex(modelindex)
			avatarpath = self.projman.getUserAvatar(userobj, html)
			if avatarpath != None:
				self.loadAvatar(avatarpath)
Example #2
0
class MainWindow(QMainWindow, ui_cghubdownloader.Ui_CGHubDownloader):
    def __init__(self, parent=None):
        super(MainWindow, self).__init__(parent)

        #  Start logging
        #  ----------------------------------------
        logger = logging.getLogger("mainwindow.__init__")

        #  Member variables and init stuffs
        #  ----------------------------------------
        self.projdir = os.path.join(os.getcwd(), 'Downloads')

        if not os.path.isdir(self.projdir):
            os.makedirs(self.projdir)

        self.addedUsers = []
        self.currentuser = ''
        self.currentusermodel = None  # holds UserImagesModel
        self.currentthumbnailsmodel = None  # holds ThumbnailsModel
        self.currentview = 0  # 0 -> Table view, 1 -> Thumbnails view
        self.setupUi(self)

        #  Factory
        #  ----------------------------------------
        self.factory = Factory()
        self.factory.addClass('UserValidation', ProcessUserValidation)
        self.factory.addWorkers('UserValidation')
        self.factory.addClass('UserImages', ProcessUserImages)
        self.factory.addWorkers('UserImages')
        self.factory.addClass('ImagesFilesize', CheckImagesFilesize)
        self.factory.addWorkers('ImagesFilesize')

        for worker in self.factory.getWorkers('UserValidation'):
            self.connect(worker, SIGNAL('error'), self.callError)
            self.connect(worker, SIGNAL('updateList'), self.refreshUserList)

        for worker in self.factory.getWorkers('UserImages'):
            self.connect(worker, SIGNAL('error'), self.callError)
            self.connect(worker, SIGNAL('updateTable'), self.refreshTableView)

        for worker in self.factory.getWorkers('ImagesFilesize'):
            self.connect(worker, SIGNAL('updateTableModel'),
                         self.updateTableModel)

        #  Users data
        #  ----------------------------------------
        self.usersdata = UsersData(self)
        self.usersdata.setProjectDir(self.projdir)
        self.usersdata.error.connect(self.callError)  # SIGNAL("error()")
        self.userslistdata = UsersList(self.usersdata)
        self.userslistdata.setProjectDir(self.projdir)
        self.userslistdata.updateFromUsersData(self.usersdata)

        for worker in self.factory.getWorkers('UserImages'):
            self.connect(worker, SIGNAL('updateTable'),
                         self.userslistdata.updateImagesCount)

        #  Project manager
        #  ----------------------------------------
        self.projman = ProjectManager(self.projdir)
        self.projman.updateWithExistingImages(self.usersdata)
        self.projman.saveData.connect(self.saveData)  # SIGNAL("saveData()")

        #  Users list
        #  ----------------------------------------
        self.userslistmodel = UsersListModel(self.userslistdata)
        self.users_listViewWidget.setModel(self.userslistmodel)
        self.users_listViewWidget.setGridSize(QSize(20, 20))
        headerstyle = "font-size: 12px;"
        self.users_listViewWidget.setStyleSheet(headerstyle)
        # Handle selection changed
        selectionmodel = self.users_listViewWidget.selectionModel()
        selectionmodel.selectionChanged.connect(
            self.userslistmodel.handleSelectionChanged)
        self.userslistmodel.onSelectionChanged.connect(self.onUserSelect)

        #  Setup layout inside stacked widget
        #  ----------------------------------------
        #  ( I can't understand how to set this up in Qt Designer,
        #    I can only create layout for one of them, then there
        #    seems to be no way to set layout for the other one )
        self.horizontalLayout_a = QHBoxLayout(self.page)
        self.horizontalLayout_a.addWidget(self.images_tableViewWidget)
        self.horizontalLayout_b = QHBoxLayout(self.page_2)
        self.horizontalLayout_b.addWidget(self.thumbnails_tableViewWidget)

        #  Table View
        #  ----------------------------------------
        self.gettablemodel = {}
        self.setTableModel('')
        #  Set table header's font size
        headerstyle = "QHeaderView { font-family: Segoe UI Light; font-size: 12px; }"
        self.images_tableViewWidget.horizontalHeader().setStyleSheet(
            headerstyle)
        self.images_tableViewWidget.verticalHeader().setStyleSheet(headerstyle)
        #  Set column width
        self.images_tableViewWidget.setColumnWidth(2, 100)
        self.images_tableViewWidget.setColumnWidth(1, 100)
        self.images_tableViewWidget.horizontalHeader().setResizeMode(
            1, QHeaderView.Fixed)
        self.images_tableViewWidget.horizontalHeader().setResizeMode(
            2, QHeaderView.Fixed)

        #  Thumbnails View
        #  ----------------------------------------
        self.getthumbnailsmodel = {}
        self.setThumbnailsModel('')
        self.thumbnails_tableViewWidget.setIconSize(QSize(64, 64))

        #  Table view popup menu
        #  ----------------------------------------
        self.table_popMenu = QMenu(self)
        checkSelected_Action = QAction('Check Selected', self)
        uncheckSelected_Action = QAction('Uncheck Selected', self)
        checkAll_Action = QAction('Check All', self)
        uncheckAll_Action = QAction('Uncheck All', self)
        viewFile_Action = QAction('View File', self)
        self.connect(checkAll_Action, SIGNAL("triggered()"),
                     partial(self.checkSelected, True))
        self.connect(uncheckAll_Action, SIGNAL("triggered()"),
                     partial(self.checkSelected, False))
        self.connect(checkAll_Action, SIGNAL("triggered()"),
                     partial(self.checkAll, True))
        self.connect(uncheckAll_Action, SIGNAL("triggered()"),
                     partial(self.checkAll, False))
        self.connect(viewFile_Action, SIGNAL("triggered()"), self.viewFile)
        self.table_popMenu.addAction(checkSelected_Action)
        self.table_popMenu.addAction(uncheckSelected_Action)
        self.table_popMenu.addSeparator()
        self.table_popMenu.addAction(checkAll_Action)
        self.table_popMenu.addAction(uncheckAll_Action)
        self.table_popMenu.addSeparator()
        self.table_popMenu.addAction(viewFile_Action)
        self.images_tableViewWidget.setContextMenuPolicy(Qt.CustomContextMenu)
        self.images_tableViewWidget.customContextMenuRequested.connect(
            self.tableviewPopup)

        #  Thumbnails view popup menu
        #  ----------------------------------------
        self.thumbnails_popMenu = QMenu(self)
        thumbCheckSelected_Action = QAction('Check Selected', self)
        thumbUncheckSelected_Action = QAction('Uncheck Selected', self)
        thumbCheckAll_Action = QAction('Check All', self)
        thumbUncheckAll_Action = QAction('Uncheck All', self)
        self.connect(thumbCheckSelected_Action, SIGNAL("triggered()"),
                     partial(self.checkSelected, True))
        self.connect(thumbUncheckSelected_Action, SIGNAL("triggered()"),
                     partial(self.checkSelected, False))
        self.connect(thumbCheckAll_Action, SIGNAL("triggered()"),
                     partial(self.checkAll, True))
        self.connect(thumbUncheckAll_Action, SIGNAL("triggered()"),
                     partial(self.checkAll, False))
        self.thumbnails_popMenu.addAction(thumbCheckSelected_Action)
        self.thumbnails_popMenu.addAction(thumbUncheckSelected_Action)
        self.thumbnails_popMenu.addSeparator()
        self.thumbnails_popMenu.addAction(thumbCheckAll_Action)
        self.thumbnails_popMenu.addAction(thumbUncheckAll_Action)
        self.thumbnails_tableViewWidget.setContextMenuPolicy(
            Qt.CustomContextMenu)
        self.thumbnails_tableViewWidget.customContextMenuRequested.connect(
            self.thumbnailsViewPopup)

        #  Users list view popup menu
        #  ----------------------------------------
        self.list_popMenu = QMenu(self)
        deleteUser_Action = QAction('Delete user', self)
        self.connect(deleteUser_Action, SIGNAL("triggered()"), self.deleteUser)
        self.list_popMenu.addAction(deleteUser_Action)
        self.users_listViewWidget.setContextMenuPolicy(Qt.CustomContextMenu)
        self.users_listViewWidget.customContextMenuRequested.connect(
            self.listviewPopup)

        #  Set project button popup menu
        #  ----------------------------------------
        self.setproject_buttonWidget.setContextMenuPolicy(Qt.CustomContextMenu)
        self.setproject_buttonWidget.customContextMenuRequested.connect(
            self.projectButtonPopup)
        self.project_popMenu = QMenu(self)
        openProjectDir_Action = QAction('Open Download location', self)
        self.connect(openProjectDir_Action, SIGNAL("triggered()"),
                     partial(self.openFolder, 'project'))
        self.project_popMenu.addAction(openProjectDir_Action)

        #  Avatar
        #  ----------------------------------------
        self.icon = QIcon()
        #  Set default avatar
        self.loadAvatar(":no_avatar100.gif")
        self.username_labelWidget.setText(self.currentuser)

        #  Log Dock Widget
        #  ----------------------------------------
        logDockWidget = QDockWidget("Log", self)
        logDockWidget.setObjectName("LogDockWidget")
        logDockWidget.setAllowedAreas(Qt.LeftDockWidgetArea
                                      | Qt.RightDockWidgetArea)
        self.logtext = LogText()
        self.log_TextBrowser = self.logtext.textbrowser
        logDockWidget.setWidget(self.log_TextBrowser)
        self.addDockWidget(Qt.RightDockWidgetArea, logDockWidget)
        self.log_TextBrowser.setStyleSheet(stylesheets.logArea)
        logDockWidget.hide()

        #  Menu Additions
        #  ----------------------------------------
        #  View menu
        viewDatabase_Action = self.createAction('View Database',
                                                self.openDatViewer, "Alt+D",
                                                None, None, False)
        switchView_Action = self.createAction('Switch view',
                                              self.userSwitchView, "Alt+Q",
                                              None, None, False)
        # self.menu_View.addAction(logDockWidget.toggleViewAction())
        self.view_Menu.addAction(switchView_Action)
        self.view_Menu.addSeparator()
        self.view_Menu.addAction(viewDatabase_Action)
        #  Help menu
        about_Action = self.createAction('About', self.showAboutDialog, "",
                                         None, None, False)
        # self.about_Action.activated.connect(self.showAboutDialog)
        self.help_Menu.addAction(about_Action)

        #  Connect Signals and Slots
        #  ----------------------------------------
        #  Main Widgets
        self.adduser_buttonWidget.pressed.connect(
            self.onAddUser)  # SIGNAL("pressed()")
        self.newuser_lineEditWidget.returnPressed.connect(
            self.onAddUser)  # SIGNAL("returnPressed()")
        self.users_listViewWidget.doubleClicked.connect(self.onUserSelect)

        # Note: I leave out activated signal because I not sure what it does
        # self.users_listViewWidget.activated.connect(self.onUserSelect)

        # Note: clicked signal is not used because we already have
        #  self.userslistmodel.onSelectionChanged.connect(self.onUserSelect).
        #  Adding both will double trigger the onUserSelect method which
        #  will cause the view be refresed twice
        # self.users_listViewWidget.clicked.connect(self.onUserSelect)

        #  Buttons on left panel
        self.setproject_buttonWidget.clicked.connect(self.setProjectLocation)
        self.download_buttonWidget.clicked.connect(self.downloadSelected)
        self.avatar_buttonWidget.clicked.connect(
            partial(self.openFolder, 'user'))
        self.tableview_buttonWidget.clicked.connect(partial(
            self.switchView, 0))
        self.thumbnailview_buttonWidget.clicked.connect(
            partial(self.switchView, 1))
        #  Actions
        self.test_action.triggered.connect(self.test)
        self.openDatViewer_action.triggered.connect(self.openDatViewer)

        #  Finalizing
        #  ----------------------------------------
        self.newuser_lineEditWidget.setFocus()
        self.resize(900, 500)
        self.setStatus("Hello", 1.5)
        self.switchView(1)  # Set view to table view
        self.setProjectDir(os.path.join(os.getcwd(), 'Downloads'))
        QTimer.singleShot(0, self.newuser_lineEditWidget, SLOT('setFocus()'))

        logger.info("Finish constructing main window")

    def createAction(self,
                     text,
                     slot=None,
                     shortcut=None,
                     icon=None,
                     tip=None,
                     checkable=False,
                     signal="triggered()"):
        # ( Copied from Rapid GUI Programming with Python and Qt,
        #		Chapter 6, imagechanger.py )
        action = QAction(text, self)
        if icon is not None:
            action.setIcon(QIcon(":/%s.png" % icon))
        if shortcut is not None:
            action.setShortcut(shortcut)
        if tip is not None:
            action.setToolTip(tip)
            action.setStatusTip(tip)
        if slot is not None:
            self.connect(action, SIGNAL(signal), slot)
        if checkable:
            action.setCheckable(True)
        return action

    #  Non-UI methods
    #  ----------------------------------------
    def test(self):
        pass

    def logCurrentMethod(self, msg):
        methodname = inspect.stack()[1][3]
        logger = logging.getLogger("mainwindow." + methodname)
        logger.info(msg)

    def printToLog(self, msg=''):
        self.logtext.addNewMsg(msg)

    def callWarning(self, msg=''):
        self.logtext.addNewMsg(msg)
        QMessageBox.warning(self, 'Warning', msg)

    def callError(self, msg=''):
        self.logtext.addNewMsg(msg)
        QMessageBox.warning(self, 'Error', msg)

    def setStatus(self, msg, secs=3):
        self.statusbar.showMessage(msg, secs * 1000)

    def saveData(self):
        self.logCurrentMethod("Saving self.usersdata")
        self.usersdata.savePickle()

    def loadAvatar(self, avatarpath):
        self.logCurrentMethod("Loading avatar: {0}".format(avatarpath))
        self.icon.addPixmap(QPixmap(avatarpath), QIcon.Normal, QIcon.Off)
        self.avatar_buttonWidget.setIcon(self.icon)
        self.avatar_buttonWidget.setIconSize(QSize(50, 50))

    def setProjectDir(self, location):
        self.logCurrentMethod("Setting project dir to: {0}".format(location))
        self.projman.setProjectDir(location)
        self.setWindowTitle('CG Hub Downloader - {0}'.format(location))

    def switchView(self, view):
        self.logCurrentMethod("Switching view to...")
        button = None

        if view == 0:
            self.logCurrentMethod("Table view")
            self.tableview_buttonWidget.setEnabled(False)
            self.thumbnailview_buttonWidget.setEnabled(True)
        else:
            self.logCurrentMethod("Thumbnails view")
            self.tableview_buttonWidget.setEnabled(True)
            self.thumbnailview_buttonWidget.setEnabled(False)

        self.currentview = view
        self.stackedWidget.setCurrentIndex(view)

    def userSwitchView(self):
        self.switchView(not self.currentview)

    #  Direct-UI methods
    #  ----------------------------------------
    def setProjectLocation(self):
        dialog = QFileDialog()
        dialog.setFileMode(QFileDialog.Directory)
        dialogdir = self.projdir  # or can use QDir.currentPath()
        directory = dialog.getExistingDirectory(self, "Set download location", \
         dialogdir, QFileDialog.ShowDirsOnly)

        if directory:
            self.setProjectDir(directory)
            self.usersdata.setProjectDir(directory)
            self.userslistdata.setProjectDir(directory)
            self.userslistdata.updateFromUsersData(self.usersdata)

            # Deselect all from users list
            for modelindex in self.users_listViewWidget.selectedIndexes():
                selectionmodel = self.users_listViewWidget.selectionModel()
                selectionmodel.select(modelindex, QItemSelectionModel.Deselect)

            # Clear table view
            self.clearTable()

            # Load default avatar
            self.loadAvatar(":no_avatar100.gif")

    def downloadSelected(self):
        if self.currentuser != None and self.currentuser != '':
            self.logCurrentMethod(
                "Downloading selected images from {0}".format(
                    self.currentuser))
            userobj = self.usersdata.getUser(self.currentuser)
            self.projman.processUser(userobj)

    def openFolder(self, folderType):
        self.logCurrentMethod("Opening folder")
        if folderType == 'project':
            call(["explorer.exe ", convertPathToExplorer(self.projdir)])
        elif folderType == 'user':
            if self.currentuser != None and self.currentuser != '':
                userdir = convertPathToExplorer(self.projdir + '\\' +
                                                self.currentuser)
                if os.path.isdir(userdir):
                    call(["explorer.exe ", userdir])

    def openDatViewer(self):
        window = datviewer2.DatViewerWindow(self)
        window.show()

    def showAboutDialog(self):
        aboutdialog = AboutDialog(self)
        aboutdialog.show()

    #  Table View related methods
    #  ----------------------------------------
    def onUserSelect(self, modelindex):
        self.logCurrentMethod("Loading user's images")
        username = self.userslistmodel.data(modelindex, Qt.DisplayRole)
        # Note: username will looks like -> "username (10)"
        username = username.split(' ')[0]
        userobj = self.usersdata.getUser(username)
        self.factory.addJob('UserImages', (userobj, self.projman))

    def refreshTableView(self, userobj):
        self.logCurrentMethod("Updating table view")
        self.projman.updateWithExistingImages(self.usersdata)

        # It might always be true, but just to be clear, please be
        # aware that both table model and thumbnails model need to
        # receive the same userobj in order to sync with each other.
        self.setTableModel(userobj.name)
        self.setThumbnailsModel(userobj.name)
        self.currentthumbnailsmodel.updateModel()

        # Load avatar
        self.loadAvatar(userobj.avatarpath)

        # Set current user
        self.currentuser = userobj.name

        # Update user name
        self.username_labelWidget.setText(self.currentuser)

        # Update Images filesize
        self.updateImagesFilesize()

        # Update/Download Thumbnails
        self.projman.processUser(userobj, thumbnails=True)

        # Redownload avatar if it's missing
        if userobj.avatarpath == None:
            avatarpath = self.projman.getUserAvatar(userobj,
                                                    userobj.userpagehtml)
            if avatarpath:
                self.loadAvatar(avatarpath)

    def setTableModel(self, username):
        """ Attact a table model to view. If no table model found for
		the user, create a new one.
		"""
        self.logCurrentMethod("Setting table model for '{0}'".format(username))
        model = None

        if username not in self.gettablemodel.keys():
            model = UserImagesModel(self.usersdata, username)
            self.gettablemodel[username] = model

            # When projman is downloading an image, it will fires
            # 'updateModel' signal. This model will catch it and
            # calls 'updateModel' (which is just a simple 'modelReset()')
            # to tell the view to update. This process shows the
            # download progress
            self.projman.updateModel.connect(model.updateModel)

        else:
            model = self.gettablemodel[username]

        self.images_tableViewWidget.setModel(model)

        # Keep self updated about the change
        self.currentusermodel = model

    def updateTableModel(self):
        self.logCurrentMethod("Asking table model to 'resetModel()'")
        self.currentusermodel.updateModel()

    def clearTable(self):
        self.logCurrentMethod(
            "Clearing table, setting table model to empty user ('')")
        self.images_tableViewWidget.clearSpans()
        self.setTableModel('')  # Set to empty user will clear the table model

    def updateImagesFilesize(self):
        if self.currentuser != None and self.currentuser != '':
            self.logCurrentMethod("Updating images file size for '{0}'".format(
                self.currentuser))
            self.projman.updateWithExistingImages(
                self.currentusermodel._usersdata)
            self.setStatus(
                "Checking {0}'s images file size...".format(self.currentuser),
                3.5)
            self.factory.addJob('ImagesFilesize',
                                (self.usersdata, self.currentuser))

    #  Thumbnails view methods
    #  ----------------------------------------
    def setThumbnailsModel(self, username):
        """ Attacth a thumbnails model to view. If no thumbnails model found for
		the user, create a new one.
		"""
        self.logCurrentMethod(
            "Setting thumbnails model for '{0}'".format(username))
        model = None

        if username not in self.getthumbnailsmodel.keys():
            # Passing a UserDir to ThumbnailsModel() so that it
            # knows where to look for the thumbnails
            userdir = self.projman.getUserDir(username)
            model = ThumbnailsModel(self.usersdata, username, userdir)
            self.getthumbnailsmodel[username] = model

            # The thumbnails table view is an instance of MyThumbnailsView,
            # MyThumbnailsView will fires the 'columnCountChange' everytime
            # the window is resized. This will tell the model to change
            # it's column counts to fit the new size.
            self.thumbnails_tableViewWidget.columnCountChange.connect(
                model.setColumnCount)

            # When projman is downloading a thumbnails, it will fires
            # 'updateThumbnailsModel' signal. This model will catch it and
            # calls 'updateModel' (which is just a simple 'modelReset()')
            # to tell the view to update.
            self.projman.updateThumbnailsModel.connect(model.updateModel)

        else:
            model = self.getthumbnailsmodel[username]

        self.thumbnails_tableViewWidget.setModel(model)

        # Keep self updated about the change
        self.currentthumbnailsmodel = model

    #  Table view right click context menu
    #  ----------------------------------------
    def tableviewPopup(self, point):
        self.table_popMenu.exec_(
            self.images_tableViewWidget.mapToGlobal(point))

    def checkSelected(self, check):
        self.logCurrentMethod("Check selected")
        view = None
        model = None
        checkstate = Qt.Checked if check else Qt.Unchecked

        if self.stackedWidget.currentIndex() == 0:
            view = self.images_tableViewWidget
            model = self.currentusermodel
        else:
            view = self.thumbnails_tableViewWidget
            model = self.currentthumbnailsmodel

        for idx in view.selectedIndexes():
            model.setData(idx, checkstate, Qt.CheckStateRole)

    def checkAll(self, check):
        self.logCurrentMethod("Check all")
        view = None
        model = None
        checkstate = Qt.Checked if check else Qt.Unchecked

        if self.stackedWidget.currentIndex() == 0:
            view = self.images_tableViewWidget
            model = self.currentusermodel
        else:
            view = self.thumbnails_tableViewWidget
            model = self.currentthumbnailsmodel

        view.selectAll()
        for idx in view.selectedIndexes():
            model.setData(idx, checkstate, Qt.CheckStateRole)
        view.selectionModel().clearSelection()

    def viewFile(self):
        # for idx in self.images_tableViewWidget.selectedIndexes():
        # 	self.currentusermodel.setData(idx, Qt.Unchecked, Qt.CheckStateRole)
        pass

    #  Thumbnails view right click context menu
    #  ----------------------------------------
    def thumbnailsViewPopup(self, point):
        self.thumbnails_popMenu.exec_(
            self.thumbnails_tableViewWidget.mapToGlobal(point))

    #  Users list view right click context menu
    #  ----------------------------------------
    def listviewPopup(self, point):
        self.list_popMenu.exec_(self.users_listViewWidget.mapToGlobal(point))

    def deleteUser(self):
        # Delete from users list
        self.userslistdata.deleteUser(self.currentuser)
        # Delete from usersdata as well
        self.usersdata.deleteUser(self.currentuser)
        self.userslistmodel.updateModel()

        # Selection the first item on the list
        modelindex = self.userslistmodel.index(0, 0)
        if modelindex:
            self.users_listViewWidget.setCurrentIndex(modelindex)
            username = self.userslistmodel.data(modelindex, Qt.DisplayRole)
            self.refreshTableView(self.usersdata.getUser(username))

    #  Set project button right click context menu
    #  -------------------------------------------
    def projectButtonPopup(self, point):
        self.project_popMenu.exec_(
            self.setproject_buttonWidget.mapToGlobal(point))

    #  User lists related methods
    #  ----------------------------------------
    def onAddUser(self):
        user = self.newuser_lineEditWidget.text()
        user = str(user).lower()
        self.logCurrentMethod("Adding '{0}' to list".format(user))
        if len(user) > 0:
            if user not in self.addedUsers:
                self.usersdata.addNewUser(user)
                userobj = self.usersdata.getUser(user)
                self.setStatus("Adding '{0}'...".format(user), 3)
                self.factory.addJob('UserValidation', userobj)

    def refreshUserList(self, userobj, html):
        self.logCurrentMethod("Refreshing users list")
        if userobj.exists and userobj.viewImageUrl == False:
            # need to mark user empty???
            self.callWarning("{0}'s gallery is empty".format(userobj.name))
            logger.warning("{0}'s gallery is empty".format(userobj.name))
        else:
            self.userslistdata.addUser(userobj)
            self.userslistmodel.updateModel()
            # Select new user
            idx = self.userslistdata.getUserIndex(userobj.name)
            if idx != None:
                modelindex = self.userslistmodel.index(idx, 0)
                self.users_listViewWidget.setCurrentIndex(modelindex)
            avatarpath = self.projman.getUserAvatar(userobj, html)
            if avatarpath != None:
                self.loadAvatar(avatarpath)
class ProjectManager(QObject):
    saveData = pyqtSignal()  # connected to mainwindow.saveData
    updateModel = pyqtSignal(
    )  # connected to mainwindow's table model's (UserImagesModel) updateModel
    updateThumbnailsModel = pyqtSignal()  # similar to above

    def __init__(self, location):
        super(ProjectManager, self).__init__()
        logger = logging.getLogger("pm.ProjectManager")
        logger.info(
            "Constructing ProjectManager with location: {0}".format(location))

        self.projdir = location
        self.usersdirs = {}  # Hold a list of 'UserDir' objects
        self.factory = Factory()
        self.factory.addClass('Download', DownloadWorker)
        self.factory.addWorkers('Download')
        self.factory.addClass('DownloadThumbnails', DownloadThumbnailsWorker)
        self.factory.addWorkers('DownloadThumbnails', 5)
        self.updateUsersDir()

        logger.info("Finish constructing ProjectManager")

    def logCurrentMethod(self, msg, logtype='info'):
        methodname = inspect.stack()[1][3]
        logger = logging.getLogger("pm.ProjectManager." + methodname)
        if logtype == 'info':
            logger.info(msg)
        elif logtype == 'debug':
            logger.debug(msg)
        elif logtype == 'warning':
            logger.warning(msg)
        elif logtype == 'error':
            logger.error(msg)

    def setProjectDir(self, location):
        if os.path.isdir(location):
            self.projdir = location
            self.logCurrentMethod(
                "Project directory set to: {0}".format(location))
        else:
            self.logCurrentMethod(
                "Project directory failed to set to: {0}".format(location),
                'error')

    def createUserDir(self, username):
        """	Create a directory for user.
		createUserDir(str) -> UserDir()
		"""
        self.logCurrentMethod("Creating UserDir for '{0}'".format(username))
        userdir = UserDir(username, self.projdir)
        self.usersdirs[username] = userdir
        return userdir

    def getDirsInProjdir(self):
        """ List all directories inside project directory.
		getUsersInProjdir() -> ['dir_1', 'dir_2', ...]
		"""
        self.logCurrentMethod("Returning a list of dirs in {0}".format(
            self.projdir))
        return [
            d for d in os.listdir(self.projdir)
            if os.path.isdir(self.projdir + '/' + d)
        ]

    def getUserDir(self, username):
        if username in self.usersdirs.keys():
            return self.usersdirs[username]
        else:
            userdir = self.createUserDir(username)
            return userdir

    def updateUsersDir(self):
        self.logCurrentMethod(
            "Updating self.usersdirs with dirs inside {0}".format(
                self.projdir))
        for dir_ in self.getDirsInProjdir():
            if self.usersdirs.has_key(dir_):
                del self.usersdirs[dir_]
            self.usersdirs[dir_] = UserDir(dir_, self.projdir)

    # def updateUsersData(self, usersdata):
    def updateWithExistingImages(self, usersdata):
        """ Check out the images that already exists in Project's User's folder,
		and update usersdata so the status of each images is displayed correctly
		in the table view.
		"""
        self.logCurrentMethod(
            "Updating usersdata user's images completed status")
        for userobj in usersdata.getUsers():
            if userobj.images == None:
                continue

            userdirobj = UserDir(userobj.name, self.projdir)

            for imgobj in userobj.images:
                exists = userdirobj.hasFile(imgobj.filename)

                if exists:
                    filepath = userdirobj.path + '/' + imgobj.filename
                    try:
                        imgobj.filesize_dl = os.path.getsize(filepath)
                        if imgobj.filesize_dl == imgobj.filesize:
                            imgobj.completed = 100.0
                    except:
                        imgobj.filesize_dl = 0

            # Also check for avatar inside user's dir
            if userobj.avatarpath != None:
                avatarfilename = os.path.basename(userobj.avatarpath)
                if not userdirobj.hasFile(avatarfilename):
                    userobj.avatarpath = None

    def processUser(self, userobj, thumbnails=False):
        """ processUser() -> True or False
		Put user's imageurls into queue, to be processed by download workers.
		Specify thumbnails to 'True' to download thumbnails.
		"""
        if thumbnails == True:
            self.logCurrentMethod(
                "Processing {0}'s thumbnails to be downloaded".format(
                    userobj.name))
        else:
            self.logCurrentMethod(
                "Processing {0}'s images to be downloaded".format(
                    userobj.name))

        if userobj == None:
            self.logCurrentMethod("'{0}' is not a valid user".format(username),
                                  'error')
            return False

        userdir = UserDir(userobj.name, self.projdir)

        if not thumbnails:
            self.logCurrentMethod(
                "Processing image list: {0}".format(userobj.images), 'debug')
            for imgobj in userobj.images:
                if imgobj.checked and \
                 (imgobj.filename not in userdir.files or imgobj.completed < 100.0 ):
                    self.logCurrentMethod(
                        "Adding '{0}' to download queue".format(
                            imgobj.filename))
                    self.factory.addJob(
                        'Download',
                        (self, userobj, userdir, imgobj, imgobj.imageurl))

        if thumbnails:
            thumbnailsnames = [t.thumbname for t in userobj.images]
            self.logCurrentMethod(
                "Processing thumbnails list: {0}".format(thumbnailsnames),
                'debug')
            for imgobj in userobj.images:
                if not imgobj.thumbname in userdir.thumbnails:
                    self.logCurrentMethod(
                        "Adding '{0}' to download queue".format(
                            imgobj.thumbname))
                    self.factory.addJob(
                        'DownloadThumbnails',
                        (self, userobj, userdir, imgobj, imgobj.thumburl))

    def downloadThumbnails(self, userobj, userdir, imgobj, thumbnailurl):
        savetofile = os.path.join(userdir.thumbnailspath, imgobj.thumbname)
        self.logCurrentMethod("Downloading '{0}' to '{1}'".format(
            thumbnailurl, savetofile))
        try:
            with open(savetofile, 'wb') as fh:
                r = requests.get(thumbnailurl, stream=True)
                fh.write(r.raw.read())
                self.logCurrentMethod("Download of '{0}' completed".format(
                    imgobj.thumbname))
        except:
            (e_type, e_val, e_traceback) = sys.exc_info()
            logger.error("Download of '{0}' stops unexpectedly".format(
                imgobj.thumbname))
            logger.error("{0}, {1}".format(e_type, e_val))
            return False
        finally:
            self.updateThumbnailsModel.emit()
            return True

    def downloadImage(self, userobj, userdir, imgobj, imageurl):
        """ downloadImage(...) -> True if success
		"""
        savetofile = os.path.join(userdir.path, imgobj.filename)
        self.logCurrentMethod("Downloading '{0}' to '{1}'".format(
            imageurl, savetofile))
        self.updateModel.emit()

        r = requests.get(imageurl, stream=True)
        filesize = int(r.headers['Content-Length'])
        filesizeKb = convertSize(filesize / 1024.0)
        self.filesize = filesize
        self.logCurrentMethod("Size of '{0}': {1}".format(
            imgobj.filename, filesizeKb))

        filesize_dl = 0
        blocksize = 8192

        with open(savetofile, 'wb') as handle:
            for block in r.iter_content(1024 * 8):
                filesize_dl += len(block)
                filesizeKb = convertSize(filesize_dl / 1024.0)
                imgobj.filesize_dl = filesize_dl
                imgobj.completed = filesize_dl * 100.0 / filesize
                self.updateModel.emit()

                if not block:
                    break

                handle.write(block)

        if imgobj.completed < 100.0:
            logger.error("Download of '{0}' stops unexpectedly".format(
                imgobj.filename))
            # imgobj.status = 'Incomplete'
        else:
            self.logCurrentMethod("Download of '{0}' completed".format(
                imgobj.filename))
            # imgobj.status = 'Saved'

        self.saveData.emit()
        self.updateModel.emit()
        userdir.updateFileList()

        return True

    def getUserAvatar(self, userobj, html):
        """ Download user avatar
		getUserAvatar(User, str) -> filepath or None
		"""
        logger = logging.getLogger("pm.ProjectManager.getUserAvatar")
        if userobj.avatarpath != None:
            return userobj.avatarpath

        soup = BeautifulSoup(html)
        avatarurl = ''
        result = soup.find_all('img', class_='userpic', id='avatarImg')
        if len(result) != 0:
            result = result[0]
            avatarurl = "http:" + result['src']
            username = userobj.name
            userdir = UserDir(username, self.projdir)
            avatarfilename = avatarurl.split('/')[-1]
            savetofile = userdir.path + '/' + avatarfilename
            self.logCurrentMethod("Downloading '{0}' to '{1}'".format(
                avatarurl, savetofile))

            r = requests.get(avatarurl, stream=True)
            filesize = int(r.headers['Content-Length'])
            filesizeKb = convertSize(filesize / 1024.0)
            self.logCurrentMethod("Size of '{0}': {1}".format(
                avatarfilename, filesizeKb))

            filesize_dl = 0
            blocksize = 8192

            with open(savetofile, 'wb') as filehandle:
                for block in r.iter_content(1024 * 8):
                    filesize_dl += len(block)
                    if not block:
                        break
                    filehandle.write(block)

            if filesize_dl == filesize:
                self.logCurrentMethod(
                    "Download of '{0}' completed".format(avatarurl))
                userobj.avatarpath = savetofile
                self.saveData.emit()
                return savetofile
            else:
                logger.error(
                    "Download of '{0}' stops unexpectedly".format(avatarurl))
                return None
class ProjectManager(QObject):
	saveData = pyqtSignal()     # connected to mainwindow.saveData
	updateModel = pyqtSignal()  # connected to mainwindow's table model's (UserImagesModel) updateModel
	updateThumbnailsModel = pyqtSignal()  # similar to above
	
	def __init__(self, location):
		super(ProjectManager, self).__init__()
		logger = logging.getLogger("pm.ProjectManager")
		logger.info("Constructing ProjectManager with location: {0}".format(location))
		
		self.projdir = location
		self.usersdirs = {}     # Hold a list of 'UserDir' objects
		self.factory = Factory()
		self.factory.addClass('Download', DownloadWorker)
		self.factory.addWorkers('Download')
		self.factory.addClass('DownloadThumbnails', DownloadThumbnailsWorker)
		self.factory.addWorkers('DownloadThumbnails', 5)
		self.updateUsersDir()
		
		logger.info("Finish constructing ProjectManager")

	def logCurrentMethod(self, msg, logtype='info'):
		methodname = inspect.stack()[1][3]
		logger = logging.getLogger("pm.ProjectManager."+methodname)
		if logtype == 'info':
			logger.info(msg)
		elif logtype == 'debug':
			logger.debug(msg)
		elif logtype == 'warning':
			logger.warning(msg)
		elif logtype == 'error':
			logger.error(msg)

	def setProjectDir(self, location):
		if os.path.isdir(location):
			self.projdir = location
			self.logCurrentMethod("Project directory set to: {0}".format(location))
		else:
			self.logCurrentMethod("Project directory failed to set to: {0}".format(location), 'error')
	
	def createUserDir(self, username):
		"""	Create a directory for user.
		createUserDir(str) -> UserDir()
		"""
		self.logCurrentMethod("Creating UserDir for '{0}'".format(username))
		userdir = UserDir(username, self.projdir)
		self.usersdirs[username] = userdir
		return userdir
	
	def getDirsInProjdir(self):
		""" List all directories inside project directory.
		getUsersInProjdir() -> ['dir_1', 'dir_2', ...]
		"""
		self.logCurrentMethod("Returning a list of dirs in {0}".format(self.projdir))
		return [ d for d in os.listdir(self.projdir) if os.path.isdir(self.projdir+'/'+d) ]
	
	def getUserDir(self, username):
		if username in self.usersdirs.keys():
			return self.usersdirs[username]
		else:
			userdir = self.createUserDir(username)
			return userdir
	
	def updateUsersDir(self):
		self.logCurrentMethod("Updating self.usersdirs with dirs inside {0}".format(self.projdir))
		for dir_ in self.getDirsInProjdir():
			if self.usersdirs.has_key(dir_):
				del self.usersdirs[dir_]
			self.usersdirs[dir_] = UserDir(dir_, self.projdir)
	
	# def updateUsersData(self, usersdata):
	def updateWithExistingImages(self, usersdata):
		""" Check out the images that already exists in Project's User's folder,
		and update usersdata so the status of each images is displayed correctly
		in the table view.
		"""
		self.logCurrentMethod("Updating usersdata user's images completed status")
		for userobj in usersdata.getUsers():
			if userobj.images == None:
				continue
			
			userdirobj = UserDir(userobj.name, self.projdir)
			
			for imgobj in userobj.images:
				exists = userdirobj.hasFile(imgobj.filename)
				
				if exists:
					filepath = userdirobj.path+'/'+imgobj.filename
					try:
						imgobj.filesize_dl = os.path.getsize(filepath)
						if imgobj.filesize_dl == imgobj.filesize:
							imgobj.completed = 100.0
					except:
						imgobj.filesize_dl = 0
		
			# Also check for avatar inside user's dir
			if userobj.avatarpath != None:
				avatarfilename = os.path.basename(userobj.avatarpath)
				if not userdirobj.hasFile(avatarfilename):
					userobj.avatarpath = None
		
	def processUser(self, userobj, thumbnails=False):
		""" processUser() -> True or False
		Put user's imageurls into queue, to be processed by download workers.
		Specify thumbnails to 'True' to download thumbnails.
		"""
		if thumbnails == True:
			self.logCurrentMethod("Processing {0}'s thumbnails to be downloaded".format(userobj.name))
		else:
			self.logCurrentMethod("Processing {0}'s images to be downloaded".format(userobj.name))
		
		if userobj == None:
			self.logCurrentMethod("'{0}' is not a valid user".format(username), 'error')
			return False
		
		userdir = UserDir(userobj.name, self.projdir)
		
		if not thumbnails:
			self.logCurrentMethod("Processing image list: {0}".format(userobj.images), 'debug')
			for imgobj in userobj.images:
				if imgobj.checked and \
					(imgobj.filename not in userdir.files or imgobj.completed < 100.0 ):
					self.logCurrentMethod("Adding '{0}' to download queue".format(imgobj.filename))
					self.factory.addJob('Download', (self, userobj, userdir, imgobj, imgobj.imageurl))

		if thumbnails:
			thumbnailsnames = [ t.thumbname for t in userobj.images ]
			self.logCurrentMethod("Processing thumbnails list: {0}".format(thumbnailsnames), 'debug')
			for imgobj in userobj.images:
				if not imgobj.thumbname in userdir.thumbnails:
					self.logCurrentMethod("Adding '{0}' to download queue".format(imgobj.thumbname))
					self.factory.addJob('DownloadThumbnails', (self, userobj, userdir, imgobj, imgobj.thumburl))

	def downloadThumbnails(self, userobj, userdir, imgobj, thumbnailurl):
		savetofile = os.path.join(userdir.thumbnailspath, imgobj.thumbname)
		self.logCurrentMethod("Downloading '{0}' to '{1}'".format(thumbnailurl, savetofile))
		try:
			with open(savetofile, 'wb') as fh:
				r = requests.get(thumbnailurl, stream=True)
				fh.write(r.raw.read())
				self.logCurrentMethod("Download of '{0}' completed".format(imgobj.thumbname))
		except:
			(e_type, e_val, e_traceback) = sys.exc_info()
			logger.error("Download of '{0}' stops unexpectedly".format(imgobj.thumbname))
			logger.error("{0}, {1}".format(e_type, e_val))
			return False
		finally:
			self.updateThumbnailsModel.emit()
			return True
	
	def downloadImage(self, userobj, userdir, imgobj, imageurl):
		""" downloadImage(...) -> True if success
		"""
		savetofile = os.path.join(userdir.path, imgobj.filename)
		self.logCurrentMethod("Downloading '{0}' to '{1}'".format(imageurl, savetofile))
		self.updateModel.emit()

		r = requests.get(imageurl, stream=True)
		filesize = int(r.headers['Content-Length'])
		filesizeKb = convertSize(filesize/1024.0)
		self.filesize = filesize
		self.logCurrentMethod("Size of '{0}': {1}".format(imgobj.filename, filesizeKb))

		filesize_dl = 0
		blocksize = 8192
		
		with open(savetofile, 'wb') as handle:
			for block in r.iter_content(1024*8):
				filesize_dl += len(block)
				filesizeKb = convertSize(filesize_dl/1024.0)
				imgobj.filesize_dl = filesize_dl
				imgobj.completed = filesize_dl*100.0/filesize
				self.updateModel.emit()
				
				if not block:
					break
				
				handle.write(block)
		
		if imgobj.completed < 100.0:
			logger.error("Download of '{0}' stops unexpectedly".format(imgobj.filename))
			# imgobj.status = 'Incomplete'
		else:
			self.logCurrentMethod("Download of '{0}' completed".format(imgobj.filename))
			# imgobj.status = 'Saved'
		
		self.saveData.emit()
		self.updateModel.emit()
		userdir.updateFileList()
		
		return True

	def getUserAvatar(self, userobj, html):
		""" Download user avatar
		getUserAvatar(User, str) -> filepath or None
		"""
		logger = logging.getLogger("pm.ProjectManager.getUserAvatar")
		if userobj.avatarpath != None:
			return userobj.avatarpath
		
		soup = BeautifulSoup(html)
		avatarurl = ''
		result = soup.find_all('img', class_='userpic', id='avatarImg')
		if len(result) != 0:
			result = result[0]
			avatarurl = "http:"+result['src']
			username = userobj.name
			userdir = UserDir(username, self.projdir)
			avatarfilename = avatarurl.split('/')[-1]
			savetofile = userdir.path+'/'+avatarfilename
			self.logCurrentMethod("Downloading '{0}' to '{1}'".format(avatarurl, savetofile))

			r = requests.get(avatarurl, stream=True)
			filesize = int(r.headers['Content-Length'])
			filesizeKb = convertSize(filesize/1024.0)
			self.logCurrentMethod("Size of '{0}': {1}".format(avatarfilename, filesizeKb))

			filesize_dl = 0
			blocksize = 8192

			with open(savetofile, 'wb') as filehandle:
				for block in r.iter_content(1024*8):
					filesize_dl += len(block)
					if not block:
						break
					filehandle.write(block)
			
			if filesize_dl == filesize:
				self.logCurrentMethod("Download of '{0}' completed".format(avatarurl))
				userobj.avatarpath = savetofile
				self.saveData.emit()
				return savetofile
			else:
				logger.error("Download of '{0}' stops unexpectedly".format(avatarurl))
				return None