diff --git a/scripts/qflatboob b/scripts/qflatboob new file mode 100755 index 0000000000000000000000000000000000000000..6975339d9aa16bd6768ecfe761a7c81026762b3c --- /dev/null +++ b/scripts/qflatboob @@ -0,0 +1,27 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# vim: ft=python et softtabstop=4 cinoptions=4 shiftwidth=4 ts=4 ai + +# Copyright(C) 2010-2012 Romain Bignon +# +# This file is part of weboob. +# +# weboob is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# weboob is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with weboob. If not, see . + + +from weboob.applications.qflatboob import QFlatBoob + + +if __name__ == '__main__': + QFlatBoob.run() diff --git a/setup.py b/setup.py index efecf1b1479103118c03a2a3cdc1e63eee11b175..4c0421680408ea485530221e16c6a32c9190ea3f 100755 --- a/setup.py +++ b/setup.py @@ -96,6 +96,7 @@ def build_qt(): if sys.platform != 'win32': subprocess.check_call(['make']+extraMakeFlag+['-C','weboob/applications/qvideoob/ui'], env=env ) subprocess.check_call(['make']+extraMakeFlag+['-C','weboob/applications/qwebcontentedit/ui'], env=env ) + subprocess.check_call(['make']+extraMakeFlag+['-C','weboob/applications/qflatboob/ui'], env=env ) subprocess.check_call(['make']+extraMakeFlag+['-C','weboob/tools/application/qt'], env=env ) class Options: @@ -145,7 +146,7 @@ class Options: packages = set(find_packages()) hildon_scripts = set(('masstransit',)) -qt_scripts = set(('qboobmsg', 'qhavesex', 'qvideoob', 'weboob-config-qt', 'qwebcontentedit')) +qt_scripts = set(('qboobmsg', 'qhavesex', 'qvideoob', 'weboob-config-qt', 'qwebcontentedit', 'qflatboob')) if not options.hildon: scripts = scripts - hildon_scripts @@ -168,6 +169,8 @@ class Options: 'weboob.applications.qweboobcfg.ui', 'weboob.applications.qwebcontentedit', 'weboob.applications.qwebcontentedit.ui' + 'weboob.applications.qflatboob', + 'weboob.applications.qflatboob.ui' )) if not options.hildon: diff --git a/weboob/applications/qflatboob/__init__.py b/weboob/applications/qflatboob/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..fba7b0ba1c33f8e2126cbb4f3821ab0eaaf2f458 --- /dev/null +++ b/weboob/applications/qflatboob/__init__.py @@ -0,0 +1,3 @@ +from .qflatboob import QFlatBoob + +__all__ = ['QFlatBoob'] diff --git a/weboob/applications/qflatboob/main_window.py b/weboob/applications/qflatboob/main_window.py new file mode 100644 index 0000000000000000000000000000000000000000..1487c3c5aa600f0fe6d13b127ece11b767161ebf --- /dev/null +++ b/weboob/applications/qflatboob/main_window.py @@ -0,0 +1,219 @@ +# -*- coding: utf-8 -*- + +# Copyright(C) 2010-2012 Romain Bignon +# +# This file is part of weboob. +# +# weboob is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# weboob is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with weboob. If not, see . + +from PyQt4.QtGui import QListWidgetItem, QImage, QPixmap, QLabel +from PyQt4.QtCore import SIGNAL, Qt + +from weboob.tools.application.qt import QtMainWindow, QtDo, HTMLDelegate +from weboob.tools.application.qt.backendcfg import BackendCfg +from weboob.capabilities.housing import ICapHousing, Query, City +from weboob.capabilities.base import NotLoaded + +from .ui.main_window_ui import Ui_MainWindow +from .query import QueryDialog + +class MainWindow(QtMainWindow): + def __init__(self, config, weboob, parent=None): + QtMainWindow.__init__(self, parent) + self.ui = Ui_MainWindow() + self.ui.setupUi(self) + + self.config = config + self.weboob = weboob + self.process = None + self.housing = None + self.displayed_photo_idx = 0 + self.process_photo = {} + + self.ui.housingsList.setItemDelegate(HTMLDelegate()) + self.ui.housingFrame.hide() + + self.connect(self.ui.actionBackends, SIGNAL("triggered()"), self.backendsConfig) + self.connect(self.ui.queriesList, SIGNAL('currentIndexChanged(int)'), self.queryChanged) + self.connect(self.ui.addQueryButton, SIGNAL('clicked()'), self.addQuery) + self.connect(self.ui.housingsList, SIGNAL('itemClicked(QListWidgetItem*)'), self.housingSelected) + self.connect(self.ui.previousButton, SIGNAL('clicked()'), self.previousClicked) + self.connect(self.ui.nextButton, SIGNAL('clicked()'), self.nextClicked) + + self.reloadQueriesList() + self.refreshHousingsList() + + if self.weboob.count_backends() == 0: + self.backendsConfig() + + def backendsConfig(self): + bckndcfg = BackendCfg(self.weboob, (ICapHousing,), self) + if bckndcfg.run(): + pass + + def reloadQueriesList(self, select_name=None): + self.disconnect(self.ui.queriesList, SIGNAL('currentIndexChanged(int)'), self.queryChanged) + self.ui.queriesList.clear() + for name in self.config.get('queries', default={}).iterkeys(): + self.ui.queriesList.addItem(name) + if name == select_name: + self.ui.queriesList.setCurrentIndex(len(self.ui.queriesList)) + self.connect(self.ui.queriesList, SIGNAL('currentIndexChanged(int)'), self.queryChanged) + + def addQuery(self): + querydlg = QueryDialog(self.weboob, self) + if querydlg.exec_(): + name = unicode(querydlg.ui.nameEdit.text()) + query = {} + query['cities'] = [] + for i in xrange(len(querydlg.ui.citiesList)): + item = querydlg.ui.citiesList.item(i) + city = item.data(Qt.UserRole).toPyObject() + query['cities'].append({'id': city.id, 'backend': city.backend, 'name': city.name}) + query['area_min'] = querydlg.ui.areaMin.value() + query['area_max'] = querydlg.ui.areaMax.value() + query['cost_min'] = querydlg.ui.costMin.value() + query['cost_max'] = querydlg.ui.costMax.value() + self.config.set('queries', name, query) + self.config.save() + + self.reloadQueriesList(name) + + def queryChanged(self, i): + self.refreshHousingsList() + + def refreshHousingsList(self): + name = unicode(self.ui.queriesList.itemText(self.ui.queriesList.currentIndex())) + q = self.config.get('queries', name) + + self.ui.housingsList.clear() + self.ui.queriesList.setEnabled(False) + + query = Query() + query.cities = [] + for c in q['cities']: + city = City(c['id']) + city.backend = c['backend'] + city.name = c['name'] + query.cities.append(city) + + query.area_min = int(q['area_min']) or None + query.area_max = int(q['area_max']) or None + query.cost_min = int(q['cost_min']) or None + query.cost_max = int(q['cost_max']) or None + + self.process = QtDo(self.weboob, self.addHousing) + self.process.do('search_housings', query) + + def addHousing(self, backend, housing): + if not backend: + self.ui.queriesList.setEnabled(True) + self.process = None + return + + item = QListWidgetItem() + item.setText(u'

%s

%s — %s%s (%s)
%s' % (housing.title, housing.date.strftime('%Y-%m-%d') if housing.date else 'Unknown', + housing.cost, housing.currency, housing.backend, housing.text)) + item.setData(Qt.UserRole, housing) + self.ui.housingsList.addItem(item) + + def housingSelected(self, item): + housing = item.data(Qt.UserRole).toPyObject() + self.ui.queriesFrame.setEnabled(False) + + self.setHousing(housing) + + self.process = QtDo(self.weboob, self.gotHousing) + self.process.do('fillobj', housing, backends=housing.backend) + + def setHousing(self, housing, nottext='Loading...'): + self.housing = housing + + self.ui.housingFrame.show() + + self.display_photo() + + self.ui.titleLabel.setText('

%s

' % housing.title) + self.ui.areaLabel.setText(u'%s m²' % housing.area) + self.ui.costLabel.setText(u'%s %s' % (housing.cost, housing.currency)) + self.ui.dateLabel.setText(housing.date.strftime('%Y-%m-%d') if housing.date else nottext) + self.ui.phoneLabel.setText(housing.phone or nottext) + self.ui.locationLabel.setText(housing.location or nottext) + self.ui.stationLabel.setText(housing.station or nottext) + + self.ui.descriptionEdit.setText(housing.text or nottext) + + while self.ui.detailsFrame.layout().count() > 0: + child = self.ui.detailsFrame.layout().takeAt(0) + child.widget().hide() + child.widget().deleteLater() + + if housing.details: + for key, value in housing.details.iteritems(): + label = QLabel(value) + label.setTextInteractionFlags(Qt.TextSelectableByMouse|Qt.LinksAccessibleByMouse) + self.ui.detailsFrame.layout().addRow('%s:' % key, label) + + def gotHousing(self, backend, housing): + if not backend: + self.ui.queriesFrame.setEnabled(True) + self.process = None + return + + self.setHousing(housing, nottext='') + + def previousClicked(self): + if len(self.housing.photos) == 0: + return + self.displayed_photo_idx = (self.displayed_photo_idx - 1) % len(self.housing.photos) + self.display_photo() + + def nextClicked(self): + if len(self.housing.photos) == 0: + return + self.displayed_photo_idx = (self.displayed_photo_idx + 1) % len(self.housing.photos) + self.display_photo() + + def display_photo(self): + if not self.housing.photos: + self.ui.photoUrlLabel.setText('') + return + + if self.displayed_photo_idx >= len(self.housing.photos): + self.displayed_photo_idx = len(self.housing.photos) - 1 + if self.displayed_photo_idx < 0: + self.ui.photoUrlLabel.setText('') + return + + photo = self.housing.photos[self.displayed_photo_idx] + if photo.data: + data = photo.data + if photo.id in self.process_photo: + self.process_photo.pop(photo.id) + else: + self.process_photo[photo.id] = QtDo(self.weboob, lambda b,p: self.display_photo()) + self.process_photo[photo.id].do('fillobj', photo, ['data'], backends=self.housing.backend) + + if photo.thumbnail_data: + data = photo.thumbnail_data + else: + return + + img = QImage.fromData(data) + img = img.scaledToWidth(self.width()/3) + + self.ui.photoLabel.setPixmap(QPixmap.fromImage(img)) + if photo.url is not NotLoaded: + text = '%s' % (photo.url, photo.url) + self.ui.photoUrlLabel.setText(text) diff --git a/weboob/applications/qflatboob/qflatboob.py b/weboob/applications/qflatboob/qflatboob.py new file mode 100644 index 0000000000000000000000000000000000000000..f30dbf179e6a5803e54a4eac702660ddff2be097 --- /dev/null +++ b/weboob/applications/qflatboob/qflatboob.py @@ -0,0 +1,41 @@ +# -*- coding: utf-8 -*- + +# Copyright(C) 2010-2012 Romain Bignon +# +# This file is part of weboob. +# +# weboob is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# weboob is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with weboob. If not, see . + + +from weboob.capabilities.housing import ICapHousing +from weboob.tools.application.qt import QtApplication +from weboob.tools.config.yamlconfig import YamlConfig + +from .main_window import MainWindow + +class QFlatBoob(QtApplication): + APPNAME = 'qflatboob' + VERSION = '0.b' + COPYRIGHT = 'Copyright(C) 2010-2012 Romain Bignon' + DESCRIPTION = 'Qt application to find housings.' + CAPS = ICapHousing + CONFIG = {'queries': {}} + + def main(self, argv): + self.load_backends(ICapHousing) + self.load_config(klass=YamlConfig) + + self.main_window = MainWindow(self.config, self.weboob) + self.main_window.show() + return self.weboob.loop() diff --git a/weboob/applications/qflatboob/query.py b/weboob/applications/qflatboob/query.py new file mode 100644 index 0000000000000000000000000000000000000000..31f21b370aa39010bcb4e273c2af3c8d0ec1d1d7 --- /dev/null +++ b/weboob/applications/qflatboob/query.py @@ -0,0 +1,77 @@ +# -*- coding: utf-8 -*- + +# Copyright(C) 2010-2012 Romain Bignon +# +# This file is part of weboob. +# +# weboob is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# weboob is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with weboob. If not, see . + +from PyQt4.QtGui import QDialog, QListWidgetItem +from PyQt4.QtCore import SIGNAL, Qt + +from weboob.tools.application.qt import QtDo, HTMLDelegate + +from .ui.query_ui import Ui_QueryDialog + +class QueryDialog(QDialog): + def __init__(self, weboob, parent=None): + QDialog.__init__(self, parent) + self.ui = Ui_QueryDialog() + self.ui.setupUi(self) + + self.weboob = weboob + self.ui.resultsList.setItemDelegate(HTMLDelegate()) + self.ui.citiesList.setItemDelegate(HTMLDelegate()) + + self.search_process = None + + self.connect(self.ui.cityEdit, SIGNAL('returnPressed()'), self.searchCity) + self.connect(self.ui.resultsList, SIGNAL('itemDoubleClicked(QListWidgetItem*)'), self.insertCity) + self.connect(self.ui.citiesList, SIGNAL('itemDoubleClicked(QListWidgetItem*)'), self.removeCity) + + def keyPressEvent(self, event): + """ + Disable handler and to prevent closing the window. + """ + event.ignore() + + def searchCity(self): + pattern = unicode(self.ui.cityEdit.text()) + self.ui.resultsList.clear() + self.ui.cityEdit.clear() + self.ui.cityEdit.setEnabled(False) + + self.search_process = QtDo(self.weboob, self.addResult) + self.search_process.do('search_city', pattern) + + def addResult(self, backend, city): + if not backend or not city: + self.search_process = None + self.ui.cityEdit.setEnabled(True) + return + item = QListWidgetItem() + item.setText('%s (%s)' % (city.name, backend.name)) + item.setData(Qt.UserRole, city) + self.ui.resultsList.addItem(item) + self.ui.resultsList.sortItems() + + def insertCity(self, i): + item = QListWidgetItem() + item.setText(i.text()) + item.setData(Qt.UserRole, i.data(Qt.UserRole)) + self.ui.citiesList.addItem(item) + + def removeCity(self, item): + print item + self.ui.citiesList.removeItemWidget(item) diff --git a/weboob/applications/qflatboob/ui/Makefile b/weboob/applications/qflatboob/ui/Makefile new file mode 100644 index 0000000000000000000000000000000000000000..f0db5154cf49182632325b99b1c6d1875e938bff --- /dev/null +++ b/weboob/applications/qflatboob/ui/Makefile @@ -0,0 +1,13 @@ +UI_FILES = $(wildcard *.ui) +UI_PY_FILES = $(UI_FILES:%.ui=%_ui.py) +PYUIC = pyuic4 + +all: $(UI_PY_FILES) + +%_ui.py: %.ui + $(PYUIC) -o $@ $^ + +clean: + rm -f *.pyc + rm -f $(UI_PY_FILES) + diff --git a/weboob/applications/qflatboob/ui/__init__.py b/weboob/applications/qflatboob/ui/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/weboob/applications/qflatboob/ui/main_window.ui b/weboob/applications/qflatboob/ui/main_window.ui new file mode 100644 index 0000000000000000000000000000000000000000..8f50d4b957e085de1437ce97549d71e533e131e4 --- /dev/null +++ b/weboob/applications/qflatboob/ui/main_window.ui @@ -0,0 +1,428 @@ + + + MainWindow + + + + 0 + 0 + 709 + 572 + + + + QFlatBoob + + + + + + + Qt::Horizontal + + + + + 0 + 0 + + + + QFrame::StyledPanel + + + QFrame::Raised + + + + + + + + + + + + + + + + + + + + + + 0 + 0 + + + + + + + + + Qt::Horizontal + + + + + 1 + 0 + + + + QFrame::StyledPanel + + + QFrame::Raised + + + + + + Qt::Vertical + + + + + + + + + <h1>Loading...</h1> + + + + + + + QFormLayout::ExpandingFieldsGrow + + + + + + 75 + true + + + + Cost + + + + + + + Loading... + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse + + + + + + + + 75 + true + + + + Area + + + + + + + + 75 + true + + + + Date + + + + + + + + 75 + true + + + + Phone + + + + + + + + 75 + true + + + + Location + + + + + + + + 75 + true + + + + Station + + + + + + + Loading... + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse + + + + + + + Loading... + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse + + + + + + + Loading... + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse + + + + + + + Loading... + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse + + + + + + + Loading... + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse + + + + + + + + + Qt::Vertical + + + QSizePolicy::Expanding + + + + 20 + 5 + + + + + + + + + + + + QFrame::StyledPanel + + + QFrame::Raised + + + + + + + + + 0 + 0 + + + + + 20 + 16777215 + + + + < + + + + + + + + + + Qt::AlignCenter + + + + + + + + 0 + 0 + + + + + 20 + 16777215 + + + + > + + + + + + + + + + + + Qt::AlignCenter + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse + + + + + + + Qt::Vertical + + + QSizePolicy::Expanding + + + + 13 + 5 + + + + + + + + + + + + + + Qt::Horizontal + + + + + 0 + 50 + + + + + + QFrame::NoFrame + + + QFrame::Plain + + + + QFormLayout::ExpandingFieldsGrow + + + 20 + + + + + + + + + + + + + + + + + 0 + 0 + 709 + 24 + + + + + + + toolBar + + + TopToolBarArea + + + false + + + + + + Backends + + + + + + diff --git a/weboob/applications/qflatboob/ui/query.ui b/weboob/applications/qflatboob/ui/query.ui new file mode 100644 index 0000000000000000000000000000000000000000..e9f68acbd673854b792c73da29d242fe1ed0e630 --- /dev/null +++ b/weboob/applications/qflatboob/ui/query.ui @@ -0,0 +1,254 @@ + + + QueryDialog + + + + 0 + 0 + 551 + 457 + + + + Add a query + + + + + + QFrame::StyledPanel + + + QFrame::Raised + + + + + + Name of this query: + + + + + + + + + + Cities + + + + + + + + Press enter to search city + + + + + + + + + + + + + + + + + + + + Area + + + + + + + + Min + + + + + + + Max + + + + + + + + + 9999 + + + 5 + + + + + + + + + + + + + + + + + + 9999 + + + 5 + + + + + + + + + + + + + + + + + + + + + Cost + + + + + + + + Min + + + + + + + Max + + + + + + + + + 99999999 + + + 100 + + + + + + + + + + + + + + + + + + 99999999 + + + 100 + + + + + + + + + + + + + + + + + + + + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + + + buttonBox + accepted() + QueryDialog + accept() + + + 248 + 254 + + + 157 + 274 + + + + + buttonBox + rejected() + QueryDialog + reject() + + + 316 + 260 + + + 286 + 274 + + + + + diff --git a/weboob/tools/application/qt/qt.py b/weboob/tools/application/qt/qt.py index f133b9fd84fbdd817797eed8b8cdf9bc2336fa0a..c3b834c72bd4203cb9753aad9b645938d278660c 100644 --- a/weboob/tools/application/qt/qt.py +++ b/weboob/tools/application/qt/qt.py @@ -201,11 +201,13 @@ def local_cb(self, backend, data): if not backend: self.disconnect(self, SIGNAL('cb'), self.local_cb) self.disconnect(self, SIGNAL('eb'), self.local_eb) + self.process = None def local_eb(self, backend, error, backtrace): self.eb(backend, error, backtrace) self.disconnect(self, SIGNAL('cb'), self.local_cb) self.disconnect(self, SIGNAL('eb'), self.local_eb) + self.process = None def thread_cb(self, backend, data): self.emit(SIGNAL('cb'), backend, data)