From f6b57b91d9af93a463b9549a6977feb48169c765 Mon Sep 17 00:00:00 2001 From: necoro <> Date: Sat, 7 Apr 2007 22:20:25 +0000 Subject: Some more functionality for the Qt-Frontend --- doc/Changelog | 1 + doc/TODO | 5 + portato/gui/gtk/dialogs.py | 2 +- portato/gui/gtk/windows.py | 8 +- portato/gui/gtk/wrapper.py | 32 ++---- portato/gui/qt/dialogs.py | 38 +++++++ portato/gui/qt/tree.py | 99 +++++++++++++++++ portato/gui/qt/ui/MainWindow.ui | 51 ++++++++- portato/gui/qt/ui/SearchDialog.ui | 78 ++++++++++++++ portato/gui/qt/windows.py | 218 ++++++++++++++++++++++++++++++++------ portato/gui/wrapper.py | 19 +--- 11 files changed, 463 insertions(+), 88 deletions(-) create mode 100644 portato/gui/qt/dialogs.py create mode 100644 portato/gui/qt/tree.py create mode 100644 portato/gui/qt/ui/SearchDialog.ui diff --git a/doc/Changelog b/doc/Changelog index 1dbfb98..72b4d53 100644 --- a/doc/Changelog +++ b/doc/Changelog @@ -1,5 +1,6 @@ next: - showed difference between unmasked and masked but unmasked by yourself +- added Qt-Frontend 0.6.1: - first plugin support diff --git a/doc/TODO b/doc/TODO index c019509..2dd6fed 100644 --- a/doc/TODO +++ b/doc/TODO @@ -3,10 +3,15 @@ Backend: - bugs in update world - fix for flag handling, when reverting flags +- save/restore queue on exit/start GUI: ===== +- ICON!!! +- Systray +- only remove already merged packages from queue + GTK: ---- - make oneshot better diff --git a/portato/gui/gtk/dialogs.py b/portato/gui/gtk/dialogs.py index 68cd629..f6573ef 100644 --- a/portato/gui/gtk/dialogs.py +++ b/portato/gui/gtk/dialogs.py @@ -35,7 +35,7 @@ def not_root_dialog (): return ret def unmask_dialog (cpv): - dialog = gtk.MessageDialog(None, gtk.DIALOG_MODAL, gtk.MESSAGE_QUESTION, gtk.BUTTONS_YES_NO, cpv+" seems to be masked.\nDo you want to unmask it and its dependencies?.\n") + dialog = gtk.MessageDialog(None, gtk.DIALOG_MODAL, gtk.MESSAGE_QUESTION, gtk.BUTTONS_YES_NO, cpv+" seems to be masked.\nDo you want to unmask it and its dependencies?") ret = dialog.run() dialog.destroy() return ret diff --git a/portato/gui/gtk/windows.py b/portato/gui/gtk/windows.py index 5b89aba..3fa39a8 100644 --- a/portato/gui/gtk/windows.py +++ b/portato/gui/gtk/windows.py @@ -32,9 +32,6 @@ from dialogs import * from wrapper import GtkTree, GtkConsole from usetips import UseTips -# for the terminal -import vte - # other import types @@ -712,7 +709,6 @@ class MainWindow (Window): # booleans self.doUpdate = False - self.packageInit = True # installed pixbuf self.instPixbuf = self.window.render_icon(gtk.STOCK_YES, gtk.ICON_SIZE_MENU) @@ -758,7 +754,7 @@ class MainWindow (Window): self.build_queue_list() # the terminal - self.console = vte.Terminal() + self.console = GtkConsole() self.termHB = self.tree.get_widget("termHB") self.build_terminal() @@ -776,7 +772,7 @@ class MainWindow (Window): # set emerge queue self.queueTree = GtkTree(self.queueList.get_model()) - self.queue = EmergeQueue(console = GtkConsole(self.console), tree = self.queueTree, db = self.db, title_update = self.title_update) + self.queue = EmergeQueue(console = self.console, tree = self.queueTree, db = self.db, title_update = self.title_update) def show_package (self, *args, **kwargs): self.packageTable.update(*args, **kwargs) diff --git a/portato/gui/gtk/wrapper.py b/portato/gui/gtk/wrapper.py index 95d8afa..bce4e07 100644 --- a/portato/gui/gtk/wrapper.py +++ b/portato/gui/gtk/wrapper.py @@ -11,6 +11,7 @@ # Written by René 'Necoro' Neumann from portato.gui.wrapper import Tree, Console +import vte class GtkTree (Tree): """The implementation of the abstract tree.""" @@ -37,7 +38,7 @@ class GtkTree (Tree): if update: string += "updating" - if version != None: + if version is not None: string += " from version %s" % version return [cpv, string] @@ -49,10 +50,10 @@ class GtkTree (Tree): return self.unmergeIt def is_in_emerge (self, it): - return self.get_path_from_iter(it).split(":")[0] == self.get_path_from_iter(self.emergeIt) + return self.tree.get_string_from_iter(it).split(":")[0] == self.tree.get_string_from_iter(self.emergeIt) def is_in_unmerge (self, it): - return self.get_path_from_iter(it).split(":")[0] == self.get_path_from_iter(self.unmergeIt) + return self.tree.get_string_from_iter(it).split(":")[0] == self.tree.get_string_from_iter(self.unmergeIt) def iter_has_parent (self, it): return (self.tree.iter_parent(it) != None) @@ -72,14 +73,11 @@ class GtkTree (Tree): def get_value (self, it, column): return self.tree.get_value(it, column) - def get_path_from_iter (self, it): - return self.tree.get_string_from_iter(it) - def append (self, parent = None, values = None): return self.tree.append(parent, values) def remove (self, it): - return self.tree.remove(it) + self.tree.remove(it) def get_original (self): return self.tree @@ -87,22 +85,6 @@ class GtkTree (Tree): def get_cpv_column (self): return self.cpv_col -class GtkConsole (Console): +class GtkConsole (vte.Terminal, Console): """The implementation of the abstract Console for GTK.""" - - def __init__ (self, console): - """Constructor. - - @param console: the original console - @type console: vte.Terminal""" - - self.console = console - - def get_window_title(self): - return self.console.get_window_title() - - def set_pty (self, pty): - self.console.set_pty(pty) - - def get_original (self): - return self.console + pass diff --git a/portato/gui/qt/dialogs.py b/portato/gui/qt/dialogs.py new file mode 100644 index 0000000..cf32439 --- /dev/null +++ b/portato/gui/qt/dialogs.py @@ -0,0 +1,38 @@ +# -*- coding: utf-8 -*- +# +# File: portato/gui/qt/dialogs.py +# This file is part of the Portato-Project, a graphical portage-frontend. +# +# Copyright (C) 2007 René 'Necoro' Neumann +# This is free software. You may redistribute copies of it under the terms of +# the GNU General Public License version 2. +# There is NO WARRANTY, to the extent permitted by law. +# +# Written by René 'Necoro' Neumann + +from PyQt4.QtGui import QMessageBox + +def io_ex_dialog (parent, ex): + string = ex.strerror + if ex.filename: + string = string+": "+ex.filename + + return QMessageBox.critical(parent, "Portato", string, QMessageBox.Ok) + +def nothing_found_dialog (parent): + return QMessageBox.information(parent, "Portato", "No packages found.", QMessageBox.Ok) + +def not_root_dialog (parent): + return QMessageBox.warning(parent, "Portato", "You are not root!", QMessageBox.Ok) + +def unmask_dialog (parent, cpv): + return QMessageBox.question(parent, "Portato", cpv+" seems to be masked.\nDo you want to unmask it and its dependencies?", QMessageBox.Yes | QMessageBox.No) + +def blocked_dialog (parent, blocked, blocks): + return QMessageBox.warning(parent, "Portato", blocked+" is blocked by "+blocks+".\nPlease unmerge the blocking package.", QMessageBox.Ok) + +def remove_deps_dialog (parent): + return QMessageBox.information(parent, "Portato", "You cannot remove dependencies. :)", QMessageBox.Ok) + +def remove_queue_dialog (parent): + return QMessageBox.question(parent, "Portato", "Do you really want to clear the whole queue?", QMessageBox.Yes | QMessageBox.No) diff --git a/portato/gui/qt/tree.py b/portato/gui/qt/tree.py new file mode 100644 index 0000000..3e64f09 --- /dev/null +++ b/portato/gui/qt/tree.py @@ -0,0 +1,99 @@ +# -*- coding: utf-8 -*- +# +# File: portato/gui/qt/tree.py +# This file is part of the Portato-Project, a graphical portage-frontend. +# +# Copyright (C) 2007 René 'Necoro' Neumann +# This is free software. You may redistribute copies of it under the terms of +# the GNU General Public License version 2. +# There is NO WARRANTY, to the extent permitted by law. +# +# Written by René 'Necoro' Neumann + +from PyQt4 import QtGui, QtCore +from portato.gui.wrapper import Tree + +class QtTree (Tree): + + def __init__ (self, treeWidget, col = 0): + + self.tree = treeWidget + self.col = col + + self.emergeIt = QtGui.QTreeWidgetItem(self.tree, ["Emerge", ""]) + self.unmergeIt = QtGui.QTreeWidgetItem(self.tree, ["Unmerge", ""]) + + def build_append_value (self, cpv, oneshot = False, update = False, version = None): + string = "" + + if oneshot: + string += "(oneshot)" + if update: string += "; " + + if update: + string += "(updating" + if version is not None: + string += "from version %s" % version + string += ")" + + return [cpv, string] + + def get_emerge_it (self): + return self.emergeIt + + def get_unmerge_it (self): + return self.unmergeIt + + def is_in_emerge (self, it): + while self.iter_has_parent(it): + it = self.parent_iter(it) + return (it == self.emergeIt) + + def is_in_unmerge (self, it): + return not self.is_in_emerge(it) + + def iter_has_parent (self, it): + return (it.parent() != None) + + def parent_iter (self, it): + return it.parent() + + def first_child_iter (self, it): + return it.child(0) + + def iter_has_children (self, it): + return (it.childCount() > 0) + + def next_iter (self, it): + iter = QtGui.QTreeWidgetItemIterator(it) + iter += 1 # next iter ... + return iter.value() + + def get_value (self, it, column): + return str(it.text(column)) + + def append (self, parent = None, values = None): + if values is None: + values = ["",""] + else: + for i in range(len(values)): + if values[i] is None: + values[i] = "" + + if parent is None: + parent = self.tree + + return QtGui.QTreeWidgetItem(parent, values) + + def remove (self, it): + # a somehow strange approach ;) - go to the parent and delete the child + parent = it.parent() + index = parent.indexOfChild(it) + parent.takeChild(index) + + def get_original (self): + return self.tree + + def get_cpv_column (self): + return self.col + diff --git a/portato/gui/qt/ui/MainWindow.ui b/portato/gui/qt/ui/MainWindow.ui index c7a5549..5a8f950 100644 --- a/portato/gui/qt/ui/MainWindow.ui +++ b/portato/gui/qt/ui/MainWindow.ui @@ -64,16 +64,23 @@ QFrame::Sunken + + QAbstractItemView::NoEditTriggers + - + + + QAbstractItemView::NoEditTriggers + + - 2 + 0 @@ -214,6 +221,9 @@ p, li { white-space: pre-wrap; } Qt::ScrollBarAsNeeded + + QAbstractItemView::NoEditTriggers + true @@ -295,7 +305,24 @@ p, li { white-space: pre-wrap; } 6 - + + + QAbstractItemView::NoEditTriggers + + + 2 + + + + 1 + + + + + 1 + + + @@ -316,7 +343,7 @@ p, li { white-space: pre-wrap; } - &Emerge + E&merge @@ -419,5 +446,21 @@ p, li { white-space: pre-wrap; } + + searchEdit + returnPressed() + searchBtn + click() + + + 287 + 49 + + + 400 + 55 + + + diff --git a/portato/gui/qt/ui/SearchDialog.ui b/portato/gui/qt/ui/SearchDialog.ui new file mode 100644 index 0000000..02c62ff --- /dev/null +++ b/portato/gui/qt/ui/SearchDialog.ui @@ -0,0 +1,78 @@ + + SearchDialog + + + Qt::ApplicationModal + + + + 0 + 0 + 246 + 97 + + + + Search... + + + + 9 + + + 6 + + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel|QDialogButtonBox::NoButton|QDialogButtonBox::Ok + + + true + + + + + + + + + buttonBox + accepted() + SearchDialog + accept() + + + 164 + 112 + + + 157 + 70 + + + + + buttonBox + rejected() + SearchDialog + reject() + + + 164 + 112 + + + 173 + 70 + + + + + diff --git a/portato/gui/qt/windows.py b/portato/gui/qt/windows.py index 16152ec..e6fc7bc 100644 --- a/portato/gui/qt/windows.py +++ b/portato/gui/qt/windows.py @@ -10,6 +10,7 @@ # # Written by René 'Necoro' Neumann +# qt4 from PyQt4 import QtGui, uic, QtCore import sip @@ -21,10 +22,14 @@ from portato.backend.exceptions import * from portato.gui.gui_helper import Database, Config, EmergeQueue +# own GUI stuff from terminal import QtConsole +from tree import QtTree +from dialogs import * UI_DIR = DATA_DIR+"ui/" +#XXX: global variables are bad app = QtGui.QApplication([]) def qCheck (check): @@ -48,15 +53,16 @@ class WindowMeta (sip.wrappertype, type): class Window: - def __init__(self): - self._qt_base.__init__(self) + def __init__(self, parent = None): + self._qt_base.__init__(self, parent) self.setupUi(self) class AboutDialog (Window): + """A window showing the "about"-informations.""" __metaclass__ = WindowMeta - def __init__ (self): - Window.__init__(self) + def __init__ (self, parent = None): + Window.__init__(self, parent) self.label.setText(""" Portato v.%s

@@ -69,6 +75,33 @@ Copyright (C) 2006-2007 René 'Necoro' Neumann <necoro@necoro.net>%s" % self.actual_package().get_cp()) + + # disable buttons when emerging is not allowed + if not self.queue or not self.doEmerge: + self.window.pkgEmergeBtn.setEnabled(False) + self.window.pkgUnmergeBtn.setEnabled(False) # first update -> show if self.window.pkgTab.isHidden(): self.window.tabWidget.insertTab(0, self.window.pkgTab, "Package") self.window.pkgTab.setHidden(False) - self.window.tabWidget.setCurrentIndex(0) + self.window.tabWidget.setCurrentIndex(self.window.PKG_PAGE) def set_combo (self): self.window.versCombo.clear() @@ -148,6 +203,40 @@ class PackageDetails: item = QtGui.QTreeWidgetItem(actual_exp_it, ["", use, system.get_use_desc(use, self.cp)]) item.setCheckState(0, qCheck(pkg.is_use_flag_enabled(use))) + def _update_keywords (self, emerge, update = False): + if emerge: + try: + try: + self.queue.append(self.actual_package().get_cpv(), unmerge = False, update = update) + except PackageNotFoundException, e: + if unmask_dialog(self.window, e[0]) == QtGui.QMessageBox.Yes : + self.queue.append(self.actual_package().get_cpv(), unmerge = False, unmask = True, update = update) + except BlockedException, e: + blocked_dialog(self.window, e[0], e[1]) + else: + try: + self.queue.append(self.actual_package().get_cpv(), unmerge = True) + except PackageNotFoundException, e: + debug("Package could not be found",e[0], error = 1) + + + def actual_package (self): + """Returns the actual selected package. + + @returns: the actual selected package + @rtype: backend.Package""" + + return self.packages[self.window.versCombo.currentIndex()] + + def cb_emerge_clicked (self): + """Callback for pressed emerge-button. Adds the package to the EmergeQueue.""" + if not am_i_root(): + not_root_dialog(self.window) + else: + self._update_keywords(True) + self.window.tabWidget.setCurrentIndex(self.window.QUEUE_PAGE) + return True + def cb_combo_changed (self, combo): """Callback for the changed ComboBox. It then rebuilds the useList and the checkboxes.""" @@ -173,15 +262,15 @@ class PackageDetails: self.window.installedCheck.setSizePolicy(hidden) self.window.maskedCheck.setSizePolicy(hidden) self.window.testingCheck.setSizePolicy(hidden) - #self.window.pkgEmergeBtn.setEnabled(False) + self.window.pkgEmergeBtn.setEnabled(False) else: # normal package self.window.missingLabel.setSizePolicy(hidden) self.window.notInSysLabel.setSizePolicy(hidden) self.window.installedCheck.setSizePolicy(shown) self.window.maskedCheck.setSizePolicy(shown) self.window.testingCheck.setSizePolicy(shown) - #if self.doEmerge: - # self.emergeBtn.set_sensitive(True) + if self.doEmerge: + self.window.pkgEmergeBtn.setEnabled(True) self.window.installedCheck.setCheckState(qCheck(pkg.is_installed())) if pkg.is_masked(use_changed = False) and not pkg.is_masked(use_changed = True): @@ -198,26 +287,23 @@ class PackageDetails: self.window.testingCheck.setCheckState(qCheck(pkg.is_testing(use_keywords = False))) -# if self.doEmerge: -# # set emerge-button-label -# if not self.actual_package().is_installed(): -# self.emergeBtn.set_label("E_merge") -# self.unmergeBtn.set_sensitive(False) -# else: -# self.emergeBtn.set_label("Re_merge") -# self.unmergeBtn.set_sensitive(True) - - def actual_package (self): - """Returns the actual selected package. - - @returns: the actual selected package - @rtype: backend.Package""" - - return self.packages[self.window.versCombo.currentIndex()] + if self.doEmerge: + # set emerge-button-label + if not self.actual_package().is_installed(): + self.window.pkgEmergeBtn.setText("E&merge") + self.window.pkgUnmergeBtn.setEnabled(False) + else: + self.window.pkgEmergeBtn.setText("Re&merge") + self.window.pkgUnmergeBtn.setEnabled(True) class MainWindow (Window): __metaclass__ = WindowMeta + + # NOTEBOOK PAGE CONSTANTS + PKG_PAGE = 0 + QUEUE_PAGE = 1 + CONSOLE_PAGE = 2 def __init__ (self): Window.__init__(self) @@ -225,12 +311,22 @@ class MainWindow (Window): self.setWindowTitle(("Portato (%s)" % VERSION)) self.statusbar.showMessage("Portato - A Portage GUI") + self.doUpdate = False self.pkgDetails = PackageDetails(self) # package db self.db = Database() self.db.populate() + # config + try: + self.cfg = Config(CONFIG_LOCATION) + except IOError, e: + io_ex_dialog(self, e) + raise + + self.cfg.modify_external_configs() + # the two lists self.build_pkg_list() self.build_cat_list() @@ -245,19 +341,29 @@ class MainWindow (Window): self.consoleTab.setLayout(self.consoleLayout) self.consoleLayout.addWidget(self.console) - QtCore.QObject.connect(self.aboutAction, QtCore.SIGNAL("triggered()"), self.cb_about_triggered) - + # build queueList + self.queueList.setHeaderLabels(["Package", "Additional infos"]) + self.queueTree = QtTree(self.queueList) + + QtCore.QObject.connect(self, QtCore.SIGNAL("doTitleUpdate"), self._title_update) + + # set emerge queue + self.queue = EmergeQueue(console = self.console, tree = self.queueTree, db = self.db, title_update = self.title_update) + self.show() + + def title_update (self, title): + self.emit(QtCore.SIGNAL("doTitleUpdate"), title) - def cb_about_triggered (self): - AboutDialog().exec_() + def _title_update (self, title): + if title == None: title = "Console" + else: title = ("Console (%s)" % title) - def cb_cat_list_selected (self, index, prev): - self.selCatName = str(index.data().toString()) - self.fill_pkg_list(self.selCatName) + self.tabWidget.setTabText(self.CONSOLE_PAGE, title) - def cb_pkg_list_selected (self, index, prev): - self.pkgDetails.update(self.selCatName+"/"+str(index.data().toString())) + def jump_to (self, cp): + """Is called when we want to jump to a specific package.""" + self.pkgDetails.update(cp, self.queue) def fill_pkg_list (self, cat): self.pkgListModel.setStringList([name for (name,inst) in self.db.get_cat(cat)]) @@ -276,6 +382,50 @@ class MainWindow (Window): self.catList.setModel(self.catListModel) self.catList.setSelectionModel(self.selCatListModel) + @QtCore.pyqtSignature("") + def on_aboutAction_triggered (self): + AboutDialog(self).exec_() + + @QtCore.pyqtSignature("") + def on_searchBtn_clicked (self): + """Do a search.""" + text = str(self.searchEdit.text()) + if text != "": + packages = system.find_all_packages(text, withVersion = False) + + if packages == []: + nothing_found_dialog(self) + else: + if len(packages) == 1: + self.jump_to(packages[0]) + else: + SearchDialog(self, packages, self.jump_to).exec_() + + @QtCore.pyqtSignature("") + def on_removeBtn_clicked (self): + """Removes a selected item in the (un)emerge-queue if possible.""" + selected = self.queueList.currentItem() + + if selected: + if not selected.parent(): # top-level + if self.queueTree.iter_has_children(selected): # and has children which can be removed :) + if remove_queue_dialog(self) == QtGui.QMessageBox.Yes : + self.queue.remove_children(selected) + self.doUpdate = False + + elif selected.parent().parent(): # this is in the 3rd level => dependency + remove_deps_dialog(self) + else: + self.queue.remove_with_children(selected) + self.doUpdate = False + + def cb_cat_list_selected (self, index, prev): + self.selCatName = str(index.data().toString()) + self.fill_pkg_list(self.selCatName) + + def cb_pkg_list_selected (self, index, prev): + self.pkgDetails.update(self.selCatName+"/"+str(index.data().toString()), self.queue) + def main (self): app.exec_() diff --git a/portato/gui/wrapper.py b/portato/gui/wrapper.py index 86f4efe..12e4a71 100644 --- a/portato/gui/wrapper.py +++ b/portato/gui/wrapper.py @@ -55,7 +55,7 @@ class Tree: @param it: the iterator @type it: Iterator - @returns: Nex iterator or None if the current iter is the last one. + @returns: Next iterator or None if the current iter is the last one. @rtype: Iterator; None""" raise NotImplementedError @@ -70,16 +70,6 @@ class Tree: @rtype: anything""" raise NotImplementedError - def get_path_from_iter(self, it): - """Returns a string defining the path to the given iterator. In this path all nodes are divided by a colon ':'. - For example: 2:4:5 could mean the 6th child of the 5th child of the 3rd element. It might also mean the 5th child of the 4th child of the 2nd element. It does not matter, where counting starts as long as it is consistent. - - @param it: the iterator - @type it: Iterator - @returns: the path string - @rtype: string""" - raise NotImplementedError - def append (self, parent = None, values = None): """Appends some values right after the given parent. If parent is None, it is appended as the first element. @@ -181,10 +171,3 @@ class Console: @returns: title of the console or None""" raise NotImplementedError - - def get_original(self): - """Returns the original console-object. - - @returns: original console-object - @rtype: console-object""" - raise NotImplementedError -- cgit v1.2.3