Patrick Williams | c124f4f | 2015-09-15 14:41:29 -0500 | [diff] [blame] | 1 | # BitBake Graphical GTK User Interface |
| 2 | # |
| 3 | # Copyright (C) 2011-2012 Intel Corporation |
| 4 | # |
| 5 | # Authored by Dongxiao Xu <dongxiao.xu@intel.com> |
| 6 | # Authored by Shane Wang <shane.wang@intel.com> |
| 7 | # |
| 8 | # This program is free software; you can redistribute it and/or modify |
| 9 | # it under the terms of the GNU General Public License version 2 as |
| 10 | # published by the Free Software Foundation. |
| 11 | # |
| 12 | # This program is distributed in the hope that it will be useful, |
| 13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of |
| 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
| 15 | # GNU General Public License for more details. |
| 16 | # |
| 17 | # You should have received a copy of the GNU General Public License along |
| 18 | # with this program; if not, write to the Free Software Foundation, Inc., |
| 19 | # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. |
| 20 | import gtk |
| 21 | import gobject |
| 22 | import os |
| 23 | import os.path |
| 24 | import sys |
| 25 | import pango, pangocairo |
| 26 | import cairo |
| 27 | import math |
| 28 | |
| 29 | from bb.ui.crumbs.hobcolor import HobColors |
| 30 | from bb.ui.crumbs.persistenttooltip import PersistentTooltip |
| 31 | |
| 32 | class hwc: |
| 33 | |
| 34 | MAIN_WIN_WIDTH = 1024 |
| 35 | MAIN_WIN_HEIGHT = 700 |
| 36 | |
| 37 | class hic: |
| 38 | |
| 39 | HOB_ICON_BASE_DIR = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), ("ui/icons/")) |
| 40 | |
| 41 | ICON_RCIPE_DISPLAY_FILE = os.path.join(HOB_ICON_BASE_DIR, ('recipe/recipe_display.png')) |
| 42 | ICON_RCIPE_HOVER_FILE = os.path.join(HOB_ICON_BASE_DIR, ('recipe/recipe_hover.png')) |
| 43 | ICON_PACKAGES_DISPLAY_FILE = os.path.join(HOB_ICON_BASE_DIR, ('packages/packages_display.png')) |
| 44 | ICON_PACKAGES_HOVER_FILE = os.path.join(HOB_ICON_BASE_DIR, ('packages/packages_hover.png')) |
| 45 | ICON_LAYERS_DISPLAY_FILE = os.path.join(HOB_ICON_BASE_DIR, ('layers/layers_display.png')) |
| 46 | ICON_LAYERS_HOVER_FILE = os.path.join(HOB_ICON_BASE_DIR, ('layers/layers_hover.png')) |
| 47 | ICON_IMAGES_DISPLAY_FILE = os.path.join(HOB_ICON_BASE_DIR, ('images/images_display.png')) |
| 48 | ICON_IMAGES_HOVER_FILE = os.path.join(HOB_ICON_BASE_DIR, ('images/images_hover.png')) |
| 49 | ICON_SETTINGS_DISPLAY_FILE = os.path.join(HOB_ICON_BASE_DIR, ('settings/settings_display.png')) |
| 50 | ICON_SETTINGS_HOVER_FILE = os.path.join(HOB_ICON_BASE_DIR, ('settings/settings_hover.png')) |
| 51 | ICON_INFO_DISPLAY_FILE = os.path.join(HOB_ICON_BASE_DIR, ('info/info_display.png')) |
| 52 | ICON_INFO_HOVER_FILE = os.path.join(HOB_ICON_BASE_DIR, ('info/info_hover.png')) |
| 53 | ICON_INDI_CONFIRM_FILE = os.path.join(HOB_ICON_BASE_DIR, ('indicators/confirmation.png')) |
| 54 | ICON_INDI_ERROR_FILE = os.path.join(HOB_ICON_BASE_DIR, ('indicators/denied.png')) |
| 55 | ICON_INDI_REMOVE_FILE = os.path.join(HOB_ICON_BASE_DIR, ('indicators/remove.png')) |
| 56 | ICON_INDI_REMOVE_HOVER_FILE = os.path.join(HOB_ICON_BASE_DIR, ('indicators/remove-hover.png')) |
| 57 | ICON_INDI_ADD_FILE = os.path.join(HOB_ICON_BASE_DIR, ('indicators/add.png')) |
| 58 | ICON_INDI_ADD_HOVER_FILE = os.path.join(HOB_ICON_BASE_DIR, ('indicators/add-hover.png')) |
| 59 | ICON_INDI_REFRESH_FILE = os.path.join(HOB_ICON_BASE_DIR, ('indicators/refresh.png')) |
| 60 | ICON_INDI_ALERT_FILE = os.path.join(HOB_ICON_BASE_DIR, ('indicators/alert.png')) |
| 61 | ICON_INDI_TICK_FILE = os.path.join(HOB_ICON_BASE_DIR, ('indicators/tick.png')) |
| 62 | ICON_INDI_INFO_FILE = os.path.join(HOB_ICON_BASE_DIR, ('indicators/info.png')) |
| 63 | |
| 64 | class HobViewTable (gtk.VBox): |
| 65 | """ |
| 66 | A VBox to contain the table for different recipe views and package view |
| 67 | """ |
| 68 | __gsignals__ = { |
| 69 | "toggled" : (gobject.SIGNAL_RUN_LAST, |
| 70 | gobject.TYPE_NONE, |
| 71 | (gobject.TYPE_PYOBJECT, |
| 72 | gobject.TYPE_STRING, |
| 73 | gobject.TYPE_INT, |
| 74 | gobject.TYPE_PYOBJECT,)), |
| 75 | "row-activated" : (gobject.SIGNAL_RUN_LAST, |
| 76 | gobject.TYPE_NONE, |
| 77 | (gobject.TYPE_PYOBJECT, |
| 78 | gobject.TYPE_PYOBJECT,)), |
| 79 | "cell-fadeinout-stopped" : (gobject.SIGNAL_RUN_LAST, |
| 80 | gobject.TYPE_NONE, |
| 81 | (gobject.TYPE_PYOBJECT, |
| 82 | gobject.TYPE_PYOBJECT, |
| 83 | gobject.TYPE_PYOBJECT,)), |
| 84 | } |
| 85 | |
| 86 | def __init__(self, columns, name): |
| 87 | gtk.VBox.__init__(self, False, 6) |
| 88 | self.table_tree = gtk.TreeView() |
| 89 | self.table_tree.set_headers_visible(True) |
| 90 | self.table_tree.set_headers_clickable(True) |
| 91 | self.table_tree.set_rules_hint(True) |
| 92 | self.table_tree.set_enable_tree_lines(True) |
| 93 | self.table_tree.get_selection().set_mode(gtk.SELECTION_SINGLE) |
| 94 | self.toggle_columns = [] |
| 95 | self.table_tree.connect("row-activated", self.row_activated_cb) |
| 96 | self.top_bar = None |
| 97 | self.tab_name = name |
| 98 | |
| 99 | for i, column in enumerate(columns): |
| 100 | col_name = column['col_name'] |
| 101 | col = gtk.TreeViewColumn(col_name) |
| 102 | col.set_clickable(True) |
| 103 | col.set_resizable(True) |
| 104 | if self.tab_name.startswith('Included'): |
| 105 | if col_name!='Included': |
| 106 | col.set_sort_column_id(column['col_id']) |
| 107 | else: |
| 108 | col.set_sort_column_id(column['col_id']) |
| 109 | if 'col_min' in column.keys(): |
| 110 | col.set_min_width(column['col_min']) |
| 111 | if 'col_max' in column.keys(): |
| 112 | col.set_max_width(column['col_max']) |
| 113 | if 'expand' in column.keys(): |
| 114 | col.set_expand(True) |
| 115 | self.table_tree.append_column(col) |
| 116 | |
| 117 | if (not 'col_style' in column.keys()) or column['col_style'] == 'text': |
| 118 | cell = gtk.CellRendererText() |
| 119 | col.pack_start(cell, True) |
| 120 | col.set_attributes(cell, text=column['col_id']) |
| 121 | if 'col_t_id' in column.keys(): |
| 122 | col.add_attribute(cell, 'font', column['col_t_id']) |
| 123 | elif column['col_style'] == 'check toggle': |
| 124 | cell = HobCellRendererToggle() |
| 125 | cell.set_property('activatable', True) |
| 126 | cell.connect("toggled", self.toggled_cb, i, self.table_tree) |
| 127 | cell.connect_render_state_changed(self.stop_cell_fadeinout_cb, self.table_tree) |
| 128 | self.toggle_id = i |
| 129 | col.pack_end(cell, True) |
| 130 | col.set_attributes(cell, active=column['col_id']) |
| 131 | self.toggle_columns.append(col_name) |
| 132 | if 'col_group' in column.keys(): |
| 133 | col.set_cell_data_func(cell, self.set_group_number_cb) |
| 134 | elif column['col_style'] == 'radio toggle': |
| 135 | cell = gtk.CellRendererToggle() |
| 136 | cell.set_property('activatable', True) |
| 137 | cell.set_radio(True) |
| 138 | cell.connect("toggled", self.toggled_cb, i, self.table_tree) |
| 139 | self.toggle_id = i |
| 140 | col.pack_end(cell, True) |
| 141 | col.set_attributes(cell, active=column['col_id']) |
| 142 | self.toggle_columns.append(col_name) |
| 143 | elif column['col_style'] == 'binb': |
| 144 | cell = gtk.CellRendererText() |
| 145 | col.pack_start(cell, True) |
| 146 | col.set_cell_data_func(cell, self.display_binb_cb, column['col_id']) |
| 147 | if 'col_t_id' in column.keys(): |
| 148 | col.add_attribute(cell, 'font', column['col_t_id']) |
| 149 | |
| 150 | self.scroll = gtk.ScrolledWindow() |
| 151 | self.scroll.set_policy(gtk.POLICY_NEVER, gtk.POLICY_AUTOMATIC) |
| 152 | self.scroll.add(self.table_tree) |
| 153 | |
| 154 | self.pack_end(self.scroll, True, True, 0) |
| 155 | |
| 156 | def add_no_result_bar(self, entry): |
| 157 | color = HobColors.KHAKI |
| 158 | self.top_bar = gtk.EventBox() |
| 159 | self.top_bar.set_size_request(-1, 70) |
| 160 | self.top_bar.modify_bg(gtk.STATE_NORMAL, gtk.gdk.color_parse(color)) |
| 161 | self.top_bar.set_flags(gtk.CAN_DEFAULT) |
| 162 | self.top_bar.grab_default() |
| 163 | |
| 164 | no_result_tab = gtk.Table(5, 20, True) |
| 165 | self.top_bar.add(no_result_tab) |
| 166 | |
| 167 | label = gtk.Label() |
| 168 | label.set_alignment(0.0, 0.5) |
| 169 | title = "No results matching your search" |
| 170 | label.set_markup("<span size='x-large'><b>%s</b></span>" % title) |
| 171 | no_result_tab.attach(label, 1, 14, 1, 4) |
| 172 | |
| 173 | clear_button = HobButton("Clear search") |
| 174 | clear_button.set_tooltip_text("Clear search query") |
| 175 | clear_button.connect('clicked', self.set_search_entry_clear_cb, entry) |
| 176 | no_result_tab.attach(clear_button, 16, 19, 1, 4) |
| 177 | |
| 178 | self.pack_start(self.top_bar, False, True, 12) |
| 179 | self.top_bar.show_all() |
| 180 | |
| 181 | def set_search_entry_clear_cb(self, button, search): |
| 182 | if search.get_editable() == True: |
| 183 | search.set_text("") |
| 184 | search.set_icon_sensitive(gtk.ENTRY_ICON_SECONDARY, False) |
| 185 | search.grab_focus() |
| 186 | |
| 187 | def display_binb_cb(self, col, cell, model, it, col_id): |
| 188 | binb = model.get_value(it, col_id) |
| 189 | # Just display the first item |
| 190 | if binb: |
| 191 | bin = binb.split(', ') |
| 192 | total_no = len(bin) |
| 193 | if total_no > 1 and bin[0] == "User Selected": |
| 194 | if total_no > 2: |
| 195 | present_binb = bin[1] + ' (+' + str(total_no - 1) + ')' |
| 196 | else: |
| 197 | present_binb = bin[1] |
| 198 | else: |
| 199 | if total_no > 1: |
| 200 | present_binb = bin[0] + ' (+' + str(total_no - 1) + ')' |
| 201 | else: |
| 202 | present_binb = bin[0] |
| 203 | cell.set_property('text', present_binb) |
| 204 | else: |
| 205 | cell.set_property('text', "") |
| 206 | return True |
| 207 | |
| 208 | def set_model(self, tree_model): |
| 209 | self.table_tree.set_model(tree_model) |
| 210 | |
| 211 | def toggle_default(self): |
| 212 | model = self.table_tree.get_model() |
| 213 | if not model: |
| 214 | return |
| 215 | iter = model.get_iter_first() |
| 216 | if iter: |
| 217 | rowpath = model.get_path(iter) |
| 218 | model[rowpath][self.toggle_id] = True |
| 219 | |
| 220 | def toggled_cb(self, cell, path, columnid, tree): |
| 221 | self.emit("toggled", cell, path, columnid, tree) |
| 222 | |
| 223 | def row_activated_cb(self, tree, path, view_column): |
| 224 | if not view_column.get_title() in self.toggle_columns: |
| 225 | self.emit("row-activated", tree.get_model(), path) |
| 226 | |
| 227 | def stop_cell_fadeinout_cb(self, ctrl, cell, tree): |
| 228 | self.emit("cell-fadeinout-stopped", ctrl, cell, tree) |
| 229 | |
| 230 | def set_group_number_cb(self, col, cell, model, iter): |
| 231 | if model and (model.iter_parent(iter) == None): |
| 232 | cell.cell_attr["number_of_children"] = model.iter_n_children(iter) |
| 233 | else: |
| 234 | cell.cell_attr["number_of_children"] = 0 |
| 235 | |
| 236 | def connect_group_selection(self, cb_func): |
| 237 | self.table_tree.get_selection().connect("changed", cb_func) |
| 238 | |
| 239 | """ |
| 240 | A method to calculate a softened value for the colour of widget when in the |
| 241 | provided state. |
| 242 | |
| 243 | widget: the widget whose style to use |
| 244 | state: the state of the widget to use the style for |
| 245 | |
| 246 | Returns a string value representing the softened colour |
| 247 | """ |
| 248 | def soften_color(widget, state=gtk.STATE_NORMAL): |
| 249 | # this colour munging routine is heavily inspired bu gdu_util_get_mix_color() |
| 250 | # from gnome-disk-utility: |
| 251 | # http://git.gnome.org/browse/gnome-disk-utility/tree/src/gdu-gtk/gdu-gtk.c?h=gnome-3-0 |
| 252 | blend = 0.7 |
| 253 | style = widget.get_style() |
| 254 | color = style.text[state] |
| 255 | color.red = color.red * blend + style.base[state].red * (1.0 - blend) |
| 256 | color.green = color.green * blend + style.base[state].green * (1.0 - blend) |
| 257 | color.blue = color.blue * blend + style.base[state].blue * (1.0 - blend) |
| 258 | return color.to_string() |
| 259 | |
| 260 | class BaseHobButton(gtk.Button): |
| 261 | """ |
| 262 | A gtk.Button subclass which follows the visual design of Hob for primary |
| 263 | action buttons |
| 264 | |
| 265 | label: the text to display as the button's label |
| 266 | """ |
| 267 | def __init__(self, label): |
| 268 | gtk.Button.__init__(self, label) |
| 269 | HobButton.style_button(self) |
| 270 | |
| 271 | @staticmethod |
| 272 | def style_button(button): |
| 273 | style = button.get_style() |
| 274 | style = gtk.rc_get_style_by_paths(gtk.settings_get_default(), 'gtk-button', 'gtk-button', gobject.TYPE_NONE) |
| 275 | |
| 276 | button.set_flags(gtk.CAN_DEFAULT) |
| 277 | button.grab_default() |
| 278 | |
| 279 | # label = "<span size='x-large'><b>%s</b></span>" % gobject.markup_escape_text(button.get_label()) |
| 280 | label = button.get_label() |
| 281 | button.set_label(label) |
| 282 | button.child.set_use_markup(True) |
| 283 | |
| 284 | class HobButton(BaseHobButton): |
| 285 | """ |
| 286 | A gtk.Button subclass which follows the visual design of Hob for primary |
| 287 | action buttons |
| 288 | |
| 289 | label: the text to display as the button's label |
| 290 | """ |
| 291 | def __init__(self, label): |
| 292 | BaseHobButton.__init__(self, label) |
| 293 | HobButton.style_button(self) |
| 294 | |
| 295 | class HobAltButton(BaseHobButton): |
| 296 | """ |
| 297 | A gtk.Button subclass which has no relief, and so is more discrete |
| 298 | """ |
| 299 | def __init__(self, label): |
| 300 | BaseHobButton.__init__(self, label) |
| 301 | HobAltButton.style_button(self) |
| 302 | |
| 303 | """ |
| 304 | A callback for the state-changed event to ensure the text is displayed |
| 305 | differently when the widget is not sensitive |
| 306 | """ |
| 307 | @staticmethod |
| 308 | def desensitise_on_state_change_cb(button, state): |
| 309 | if not button.get_property("sensitive"): |
| 310 | HobAltButton.set_text(button, False) |
| 311 | else: |
| 312 | HobAltButton.set_text(button, True) |
| 313 | |
| 314 | """ |
| 315 | Set the button label with an appropriate colour for the current widget state |
| 316 | """ |
| 317 | @staticmethod |
| 318 | def set_text(button, sensitive=True): |
| 319 | if sensitive: |
| 320 | colour = HobColors.PALE_BLUE |
| 321 | else: |
| 322 | colour = HobColors.LIGHT_GRAY |
| 323 | button.set_label("<span size='large' color='%s'><b>%s</b></span>" % (colour, gobject.markup_escape_text(button.text))) |
| 324 | button.child.set_use_markup(True) |
| 325 | |
| 326 | class HobImageButton(gtk.Button): |
| 327 | """ |
| 328 | A gtk.Button with an icon and two rows of text, the second of which is |
| 329 | displayed in a blended colour. |
| 330 | |
| 331 | primary_text: the main button label |
| 332 | secondary_text: optional second line of text |
| 333 | icon_path: path to the icon file to display on the button |
| 334 | """ |
| 335 | def __init__(self, primary_text, secondary_text="", icon_path="", hover_icon_path=""): |
| 336 | gtk.Button.__init__(self) |
| 337 | self.set_relief(gtk.RELIEF_NONE) |
| 338 | |
| 339 | self.icon_path = icon_path |
| 340 | self.hover_icon_path = hover_icon_path |
| 341 | |
| 342 | hbox = gtk.HBox(False, 10) |
| 343 | hbox.show() |
| 344 | self.add(hbox) |
| 345 | self.icon = gtk.Image() |
| 346 | self.icon.set_from_file(self.icon_path) |
| 347 | self.icon.set_alignment(0.5, 0.0) |
| 348 | self.icon.show() |
| 349 | if self.hover_icon_path and len(self.hover_icon_path): |
| 350 | self.connect("enter-notify-event", self.set_hover_icon_cb) |
| 351 | self.connect("leave-notify-event", self.set_icon_cb) |
| 352 | hbox.pack_start(self.icon, False, False, 0) |
| 353 | label = gtk.Label() |
| 354 | label.set_alignment(0.0, 0.5) |
| 355 | colour = soften_color(label) |
| 356 | mark = "<span size='x-large'>%s</span>\n<span size='medium' fgcolor='%s' weight='ultralight'>%s</span>" % (primary_text, colour, secondary_text) |
| 357 | label.set_markup(mark) |
| 358 | label.show() |
| 359 | hbox.pack_start(label, True, True, 0) |
| 360 | |
| 361 | def set_hover_icon_cb(self, widget, event): |
| 362 | self.icon.set_from_file(self.hover_icon_path) |
| 363 | |
| 364 | def set_icon_cb(self, widget, event): |
| 365 | self.icon.set_from_file(self.icon_path) |
| 366 | |
| 367 | class HobInfoButton(gtk.EventBox): |
| 368 | """ |
| 369 | This class implements a button-like widget per the Hob visual and UX designs |
| 370 | which will display a persistent tooltip, with the contents of tip_markup, when |
| 371 | clicked. |
| 372 | |
| 373 | tip_markup: the Pango Markup to be displayed in the persistent tooltip |
| 374 | """ |
| 375 | def __init__(self, tip_markup, parent=None): |
| 376 | gtk.EventBox.__init__(self) |
| 377 | self.image = gtk.Image() |
| 378 | self.image.set_from_file( |
| 379 | hic.ICON_INFO_DISPLAY_FILE) |
| 380 | self.image.show() |
| 381 | self.add(self.image) |
| 382 | self.tip_markup = tip_markup |
| 383 | self.my_parent = parent |
| 384 | |
| 385 | self.set_events(gtk.gdk.BUTTON_RELEASE | |
| 386 | gtk.gdk.ENTER_NOTIFY_MASK | |
| 387 | gtk.gdk.LEAVE_NOTIFY_MASK) |
| 388 | |
| 389 | self.connect("button-release-event", self.button_release_cb) |
| 390 | self.connect("enter-notify-event", self.mouse_in_cb) |
| 391 | self.connect("leave-notify-event", self.mouse_out_cb) |
| 392 | |
| 393 | """ |
| 394 | When the mouse click is released emulate a button-click and show the associated |
| 395 | PersistentTooltip |
| 396 | """ |
| 397 | def button_release_cb(self, widget, event): |
| 398 | from bb.ui.crumbs.hig.propertydialog import PropertyDialog |
| 399 | self.dialog = PropertyDialog(title = '', |
| 400 | parent = self.my_parent, |
| 401 | information = self.tip_markup, |
| 402 | flags = gtk.DIALOG_DESTROY_WITH_PARENT |
| 403 | | gtk.DIALOG_NO_SEPARATOR) |
| 404 | |
| 405 | button = self.dialog.add_button("Close", gtk.RESPONSE_CANCEL) |
| 406 | HobAltButton.style_button(button) |
| 407 | button.connect("clicked", lambda w: self.dialog.destroy()) |
| 408 | self.dialog.show_all() |
| 409 | self.dialog.run() |
| 410 | |
| 411 | """ |
| 412 | Change to the prelight image when the mouse enters the widget |
| 413 | """ |
| 414 | def mouse_in_cb(self, widget, event): |
| 415 | self.image.set_from_file(hic.ICON_INFO_HOVER_FILE) |
| 416 | |
| 417 | """ |
| 418 | Change to the stock image when the mouse enters the widget |
| 419 | """ |
| 420 | def mouse_out_cb(self, widget, event): |
| 421 | self.image.set_from_file(hic.ICON_INFO_DISPLAY_FILE) |
| 422 | |
| 423 | class HobIndicator(gtk.DrawingArea): |
| 424 | def __init__(self, count): |
| 425 | gtk.DrawingArea.__init__(self) |
| 426 | # Set no window for transparent background |
| 427 | self.set_has_window(False) |
| 428 | self.set_size_request(38,38) |
| 429 | # We need to pass through button clicks |
| 430 | self.add_events(gtk.gdk.BUTTON_PRESS_MASK | gtk.gdk.BUTTON_RELEASE_MASK) |
| 431 | |
| 432 | self.connect('expose-event', self.expose) |
| 433 | |
| 434 | self.count = count |
| 435 | self.color = HobColors.GRAY |
| 436 | |
| 437 | def expose(self, widget, event): |
| 438 | if self.count and self.count > 0: |
| 439 | ctx = widget.window.cairo_create() |
| 440 | |
| 441 | x, y, w, h = self.allocation |
| 442 | |
| 443 | ctx.set_operator(cairo.OPERATOR_OVER) |
| 444 | ctx.set_source_color(gtk.gdk.color_parse(self.color)) |
| 445 | ctx.translate(w/2, h/2) |
| 446 | ctx.arc(x, y, min(w,h)/2 - 2, 0, 2*math.pi) |
| 447 | ctx.fill_preserve() |
| 448 | |
| 449 | layout = self.create_pango_layout(str(self.count)) |
| 450 | textw, texth = layout.get_pixel_size() |
| 451 | x = (w/2)-(textw/2) + x |
| 452 | y = (h/2) - (texth/2) + y |
| 453 | ctx.move_to(x, y) |
| 454 | self.window.draw_layout(self.style.light_gc[gtk.STATE_NORMAL], int(x), int(y), layout) |
| 455 | |
| 456 | def set_count(self, count): |
| 457 | self.count = count |
| 458 | |
| 459 | def set_active(self, active): |
| 460 | if active: |
| 461 | self.color = HobColors.DEEP_RED |
| 462 | else: |
| 463 | self.color = HobColors.GRAY |
| 464 | |
| 465 | class HobTabLabel(gtk.HBox): |
| 466 | def __init__(self, text, count=0): |
| 467 | gtk.HBox.__init__(self, False, 0) |
| 468 | self.indicator = HobIndicator(count) |
| 469 | self.indicator.show() |
| 470 | self.pack_end(self.indicator, False, False) |
| 471 | self.lbl = gtk.Label(text) |
| 472 | self.lbl.set_alignment(0.0, 0.5) |
| 473 | self.lbl.show() |
| 474 | self.pack_end(self.lbl, True, True, 6) |
| 475 | |
| 476 | def set_count(self, count): |
| 477 | self.indicator.set_count(count) |
| 478 | |
| 479 | def set_active(self, active=True): |
| 480 | self.indicator.set_active(active) |
| 481 | |
| 482 | class HobNotebook(gtk.Notebook): |
| 483 | def __init__(self): |
| 484 | gtk.Notebook.__init__(self) |
| 485 | self.set_property('homogeneous', True) |
| 486 | |
| 487 | self.pages = [] |
| 488 | |
| 489 | self.search = None |
| 490 | self.search_focus = False |
| 491 | self.page_changed = False |
| 492 | |
| 493 | self.connect("switch-page", self.page_changed_cb) |
| 494 | |
| 495 | self.show_all() |
| 496 | |
| 497 | def page_changed_cb(self, nb, page, page_num): |
| 498 | for p, lbl in enumerate(self.pages): |
| 499 | if p == page_num: |
| 500 | lbl.set_active() |
| 501 | else: |
| 502 | lbl.set_active(False) |
| 503 | |
| 504 | if self.search: |
| 505 | self.page_changed = True |
| 506 | self.reset_entry(self.search, page_num) |
| 507 | |
| 508 | def append_page(self, child, tab_label, tab_tooltip=None): |
| 509 | label = HobTabLabel(tab_label) |
| 510 | if tab_tooltip: |
| 511 | label.set_tooltip_text(tab_tooltip) |
| 512 | label.set_active(False) |
| 513 | self.pages.append(label) |
| 514 | gtk.Notebook.append_page(self, child, label) |
| 515 | |
| 516 | def set_entry(self, names, tips): |
| 517 | self.search = gtk.Entry() |
| 518 | self.search_names = names |
| 519 | self.search_tips = tips |
| 520 | style = self.search.get_style() |
| 521 | style.text[gtk.STATE_NORMAL] = self.get_colormap().alloc_color(HobColors.GRAY, False, False) |
| 522 | self.search.set_style(style) |
| 523 | self.search.set_text(names[0]) |
| 524 | self.search.set_tooltip_text(self.search_tips[0]) |
| 525 | self.search.props.has_tooltip = True |
| 526 | |
| 527 | self.search.set_editable(False) |
| 528 | self.search.set_icon_from_stock(gtk.ENTRY_ICON_SECONDARY, gtk.STOCK_CLEAR) |
| 529 | self.search.set_icon_sensitive(gtk.ENTRY_ICON_SECONDARY, False) |
| 530 | self.search.connect("icon-release", self.set_search_entry_clear_cb) |
| 531 | self.search.set_width_chars(30) |
| 532 | self.search.show() |
| 533 | |
| 534 | self.search.connect("focus-in-event", self.set_search_entry_editable_cb) |
| 535 | self.search.connect("focus-out-event", self.set_search_entry_reset_cb) |
| 536 | self.set_action_widget(self.search, gtk.PACK_END) |
| 537 | |
| 538 | def show_indicator_icon(self, title, number): |
| 539 | for child in self.pages: |
| 540 | if child.lbl.get_label() == title: |
| 541 | child.set_count(number) |
| 542 | |
| 543 | def hide_indicator_icon(self, title): |
| 544 | for child in self.pages: |
| 545 | if child.lbl.get_label() == title: |
| 546 | child.set_count(0) |
| 547 | |
| 548 | def set_search_entry_editable_cb(self, search, event): |
| 549 | self.search_focus = True |
| 550 | search.set_editable(True) |
| 551 | text = search.get_text() |
| 552 | if text in self.search_names: |
| 553 | search.set_text("") |
| 554 | style = self.search.get_style() |
| 555 | style.text[gtk.STATE_NORMAL] = self.get_colormap().alloc_color(HobColors.BLACK, False, False) |
| 556 | search.set_style(style) |
| 557 | |
| 558 | def set_search_entry_reset_cb(self, search, event): |
| 559 | page_num = self.get_current_page() |
| 560 | text = search.get_text() |
| 561 | if not text: |
| 562 | self.reset_entry(search, page_num) |
| 563 | |
| 564 | def reset_entry(self, entry, page_num): |
| 565 | style = entry.get_style() |
| 566 | style.text[gtk.STATE_NORMAL] = self.get_colormap().alloc_color(HobColors.GRAY, False, False) |
| 567 | entry.set_style(style) |
| 568 | entry.set_text(self.search_names[page_num]) |
| 569 | entry.set_tooltip_text(self.search_tips[page_num]) |
| 570 | entry.set_editable(False) |
| 571 | entry.set_icon_sensitive(gtk.ENTRY_ICON_SECONDARY, False) |
| 572 | |
| 573 | def set_search_entry_clear_cb(self, search, icon_pos, event): |
| 574 | if search.get_editable() == True: |
| 575 | search.set_text("") |
| 576 | search.set_icon_sensitive(gtk.ENTRY_ICON_SECONDARY, False) |
| 577 | search.grab_focus() |
| 578 | |
| 579 | def set_page(self, title): |
| 580 | for child in self.pages: |
| 581 | if child.lbl.get_label() == title: |
| 582 | child.grab_focus() |
| 583 | self.set_current_page(self.pages.index(child)) |
| 584 | return |
| 585 | |
| 586 | class HobWarpCellRendererText(gtk.CellRendererText): |
| 587 | def __init__(self, col_number): |
| 588 | gtk.CellRendererText.__init__(self) |
| 589 | self.set_property("wrap-mode", pango.WRAP_WORD_CHAR) |
| 590 | self.set_property("wrap-width", 300) # default value wrap width is 300 |
| 591 | self.col_n = col_number |
| 592 | |
| 593 | def do_render(self, window, widget, background_area, cell_area, expose_area, flags): |
| 594 | if widget: |
| 595 | self.props.wrap_width = self.get_resized_wrap_width(widget, widget.get_column(self.col_n)) |
| 596 | return gtk.CellRendererText.do_render(self, window, widget, background_area, cell_area, expose_area, flags) |
| 597 | |
| 598 | def get_resized_wrap_width(self, treeview, column): |
| 599 | otherCols = [] |
| 600 | for col in treeview.get_columns(): |
| 601 | if col != column: |
| 602 | otherCols.append(col) |
| 603 | adjwidth = treeview.allocation.width - sum(c.get_width() for c in otherCols) |
| 604 | adjwidth -= treeview.style_get_property("horizontal-separator") * 4 |
| 605 | if self.props.wrap_width == adjwidth or adjwidth <= 0: |
| 606 | adjwidth = self.props.wrap_width |
| 607 | return adjwidth |
| 608 | |
| 609 | gobject.type_register(HobWarpCellRendererText) |
| 610 | |
| 611 | class HobIconChecker(hic): |
| 612 | def set_hob_icon_to_stock_icon(self, file_path, stock_id=""): |
| 613 | try: |
| 614 | pixbuf = gtk.gdk.pixbuf_new_from_file(file_path) |
| 615 | except Exception, e: |
| 616 | return None |
| 617 | |
| 618 | if stock_id and (gtk.icon_factory_lookup_default(stock_id) == None): |
| 619 | icon_factory = gtk.IconFactory() |
| 620 | icon_factory.add_default() |
| 621 | icon_factory.add(stock_id, gtk.IconSet(pixbuf)) |
| 622 | gtk.stock_add([(stock_id, '_label', 0, 0, '')]) |
| 623 | |
| 624 | return icon_factory.lookup(stock_id) |
| 625 | |
| 626 | return None |
| 627 | |
| 628 | """ |
| 629 | For make hob icon consistently by request, and avoid icon view diff by system or gtk version, we use some 'hob icon' to replace the 'gtk icon'. |
| 630 | this function check the stock_id and make hob_id to replaced the gtk_id then return it or "" |
| 631 | """ |
| 632 | def check_stock_icon(self, stock_name=""): |
| 633 | HOB_CHECK_STOCK_NAME = { |
| 634 | ('hic-dialog-info', 'gtk-dialog-info', 'dialog-info') : self.ICON_INDI_INFO_FILE, |
| 635 | ('hic-ok', 'gtk-ok', 'ok') : self.ICON_INDI_TICK_FILE, |
| 636 | ('hic-dialog-error', 'gtk-dialog-error', 'dialog-error') : self.ICON_INDI_ERROR_FILE, |
| 637 | ('hic-dialog-warning', 'gtk-dialog-warning', 'dialog-warning') : self.ICON_INDI_ALERT_FILE, |
| 638 | ('hic-task-refresh', 'gtk-execute', 'execute') : self.ICON_INDI_REFRESH_FILE, |
| 639 | } |
| 640 | valid_stock_id = stock_name |
| 641 | if stock_name: |
| 642 | for names, path in HOB_CHECK_STOCK_NAME.iteritems(): |
| 643 | if stock_name in names: |
| 644 | valid_stock_id = names[0] |
| 645 | if not gtk.icon_factory_lookup_default(valid_stock_id): |
| 646 | self.set_hob_icon_to_stock_icon(path, valid_stock_id) |
| 647 | |
| 648 | return valid_stock_id |
| 649 | |
| 650 | class HobCellRendererController(gobject.GObject): |
| 651 | (MODE_CYCLE_RUNNING, MODE_ONE_SHORT) = range(2) |
| 652 | __gsignals__ = { |
| 653 | "run-timer-stopped" : (gobject.SIGNAL_RUN_LAST, |
| 654 | gobject.TYPE_NONE, |
| 655 | ()), |
| 656 | } |
| 657 | def __init__(self, runningmode=MODE_CYCLE_RUNNING, is_draw_row=False): |
| 658 | gobject.GObject.__init__(self) |
| 659 | self.timeout_id = None |
| 660 | self.current_angle_pos = 0.0 |
| 661 | self.step_angle = 0.0 |
| 662 | self.tree_headers_height = 0 |
| 663 | self.running_cell_areas = [] |
| 664 | self.running_mode = runningmode |
| 665 | self.is_queue_draw_row_area = is_draw_row |
| 666 | self.force_stop_enable = False |
| 667 | |
| 668 | def is_active(self): |
| 669 | if self.timeout_id: |
| 670 | return True |
| 671 | else: |
| 672 | return False |
| 673 | |
| 674 | def reset_run(self): |
| 675 | self.force_stop() |
| 676 | self.running_cell_areas = [] |
| 677 | self.current_angle_pos = 0.0 |
| 678 | self.step_angle = 0.0 |
| 679 | |
| 680 | ''' time_iterval: (1~1000)ms, which will be as the basic interval count for timer |
| 681 | init_usrdata: the current data which related the progress-bar will be at |
| 682 | min_usrdata: the range of min of user data |
| 683 | max_usrdata: the range of max of user data |
| 684 | step: each step which you want to progress |
| 685 | Note: the init_usrdata should in the range of from min to max, and max should > min |
| 686 | step should < (max - min) |
| 687 | ''' |
| 688 | def start_run(self, time_iterval, init_usrdata, min_usrdata, max_usrdata, step, tree): |
| 689 | if (not time_iterval) or (not max_usrdata): |
| 690 | return |
| 691 | usr_range = (max_usrdata - min_usrdata) * 1.0 |
| 692 | self.current_angle_pos = (init_usrdata * 1.0) / usr_range |
| 693 | self.step_angle = (step * 1) / usr_range |
| 694 | self.timeout_id = gobject.timeout_add(int(time_iterval), |
| 695 | self.make_image_on_progressing_cb, tree) |
| 696 | self.tree_headers_height = self.get_treeview_headers_height(tree) |
| 697 | self.force_stop_enable = False |
| 698 | |
| 699 | def force_stop(self): |
| 700 | self.emit("run-timer-stopped") |
| 701 | self.force_stop_enable = True |
| 702 | if self.timeout_id: |
| 703 | if gobject.source_remove(self.timeout_id): |
| 704 | self.timeout_id = None |
| 705 | |
| 706 | def on_draw_pixbuf_cb(self, pixbuf, cr, x, y, img_width, img_height, do_refresh=True): |
| 707 | if pixbuf: |
| 708 | r = max(img_width/2, img_height/2) |
| 709 | cr.translate(x + r, y + r) |
| 710 | if do_refresh: |
| 711 | cr.rotate(2 * math.pi * self.current_angle_pos) |
| 712 | |
| 713 | cr.set_source_pixbuf(pixbuf, -img_width/2, -img_height/2) |
| 714 | cr.paint() |
| 715 | |
| 716 | def on_draw_fadeinout_cb(self, cr, color, x, y, width, height, do_fadeout=True): |
| 717 | if do_fadeout: |
| 718 | alpha = self.current_angle_pos * 0.8 |
| 719 | else: |
| 720 | alpha = (1.0 - self.current_angle_pos) * 0.8 |
| 721 | |
| 722 | cr.set_source_rgba(color.red, color.green, color.blue, alpha) |
| 723 | cr.rectangle(x, y, width, height) |
| 724 | cr.fill() |
| 725 | |
| 726 | def get_treeview_headers_height(self, tree): |
| 727 | if tree and (tree.get_property("headers-visible") == True): |
| 728 | height = tree.get_allocation().height - tree.get_bin_window().get_size()[1] |
| 729 | return height |
| 730 | |
| 731 | return 0 |
| 732 | |
| 733 | def make_image_on_progressing_cb(self, tree): |
| 734 | self.current_angle_pos += self.step_angle |
| 735 | if self.running_mode == self.MODE_CYCLE_RUNNING: |
| 736 | if (self.current_angle_pos >= 1): |
| 737 | self.current_angle_pos = 0 |
| 738 | else: |
| 739 | if self.current_angle_pos > 1: |
| 740 | self.force_stop() |
| 741 | return False |
| 742 | |
| 743 | if self.is_queue_draw_row_area: |
| 744 | for path in self.running_cell_areas: |
| 745 | rect = tree.get_cell_area(path, tree.get_column(0)) |
| 746 | row_x, _, row_width, _ = tree.get_visible_rect() |
| 747 | tree.queue_draw_area(row_x, rect.y + self.tree_headers_height, row_width, rect.height) |
| 748 | else: |
| 749 | for rect in self.running_cell_areas: |
| 750 | tree.queue_draw_area(rect.x, rect.y + self.tree_headers_height, rect.width, rect.height) |
| 751 | |
| 752 | return (not self.force_stop_enable) |
| 753 | |
| 754 | def append_running_cell_area(self, cell_area): |
| 755 | if cell_area and (cell_area not in self.running_cell_areas): |
| 756 | self.running_cell_areas.append(cell_area) |
| 757 | |
| 758 | def remove_running_cell_area(self, cell_area): |
| 759 | if cell_area in self.running_cell_areas: |
| 760 | self.running_cell_areas.remove(cell_area) |
| 761 | if not self.running_cell_areas: |
| 762 | self.reset_run() |
| 763 | |
| 764 | gobject.type_register(HobCellRendererController) |
| 765 | |
| 766 | class HobCellRendererPixbuf(gtk.CellRendererPixbuf): |
| 767 | def __init__(self): |
| 768 | gtk.CellRendererPixbuf.__init__(self) |
| 769 | self.control = HobCellRendererController() |
| 770 | # add icon checker for make the gtk-icon transfer to hob-icon |
| 771 | self.checker = HobIconChecker() |
| 772 | self.set_property("stock-size", gtk.ICON_SIZE_DND) |
| 773 | |
| 774 | def get_pixbuf_from_stock_icon(self, widget, stock_id="", size=gtk.ICON_SIZE_DIALOG): |
| 775 | if widget and stock_id and gtk.icon_factory_lookup_default(stock_id): |
| 776 | return widget.render_icon(stock_id, size) |
| 777 | |
| 778 | return None |
| 779 | |
| 780 | def set_icon_name_to_id(self, new_name): |
| 781 | if new_name and type(new_name) == str: |
| 782 | # check the name is need to transfer to hob icon or not |
| 783 | name = self.checker.check_stock_icon(new_name) |
| 784 | if name.startswith("hic") or name.startswith("gtk"): |
| 785 | stock_id = name |
| 786 | else: |
| 787 | stock_id = 'gtk-' + name |
| 788 | |
| 789 | return stock_id |
| 790 | |
| 791 | ''' render cell exactly, "icon-name" is priority |
| 792 | if use the 'hic-task-refresh' will make the pix animation |
| 793 | if 'pix' will change the pixbuf for it from the pixbuf or image. |
| 794 | ''' |
| 795 | def do_render(self, window, tree, background_area,cell_area, expose_area, flags): |
| 796 | if (not self.control) or (not tree): |
| 797 | return |
| 798 | |
| 799 | x, y, w, h = self.on_get_size(tree, cell_area) |
| 800 | x += cell_area.x |
| 801 | y += cell_area.y |
| 802 | w -= 2 * self.get_property("xpad") |
| 803 | h -= 2 * self.get_property("ypad") |
| 804 | |
| 805 | stock_id = "" |
| 806 | if self.props.icon_name: |
| 807 | stock_id = self.set_icon_name_to_id(self.props.icon_name) |
| 808 | elif self.props.stock_id: |
| 809 | stock_id = self.props.stock_id |
| 810 | elif self.props.pixbuf: |
| 811 | pix = self.props.pixbuf |
| 812 | else: |
| 813 | return |
| 814 | |
| 815 | if stock_id: |
| 816 | pix = self.get_pixbuf_from_stock_icon(tree, stock_id, self.props.stock_size) |
| 817 | if stock_id == 'hic-task-refresh': |
| 818 | self.control.append_running_cell_area(cell_area) |
| 819 | if self.control.is_active(): |
| 820 | self.control.on_draw_pixbuf_cb(pix, window.cairo_create(), x, y, w, h, True) |
| 821 | else: |
| 822 | self.control.start_run(200, 0, 0, 1000, 150, tree) |
| 823 | else: |
| 824 | self.control.remove_running_cell_area(cell_area) |
| 825 | self.control.on_draw_pixbuf_cb(pix, window.cairo_create(), x, y, w, h, False) |
| 826 | |
| 827 | def on_get_size(self, widget, cell_area): |
| 828 | if self.props.icon_name or self.props.pixbuf or self.props.stock_id: |
| 829 | w, h = gtk.icon_size_lookup(self.props.stock_size) |
| 830 | calc_width = self.get_property("xpad") * 2 + w |
| 831 | calc_height = self.get_property("ypad") * 2 + h |
| 832 | x_offset = 0 |
| 833 | y_offset = 0 |
| 834 | if cell_area and w > 0 and h > 0: |
| 835 | x_offset = self.get_property("xalign") * (cell_area.width - calc_width - self.get_property("xpad")) |
| 836 | y_offset = self.get_property("yalign") * (cell_area.height - calc_height - self.get_property("ypad")) |
| 837 | |
| 838 | return x_offset, y_offset, w, h |
| 839 | |
| 840 | return 0, 0, 0, 0 |
| 841 | |
| 842 | gobject.type_register(HobCellRendererPixbuf) |
| 843 | |
| 844 | class HobCellRendererToggle(gtk.CellRendererToggle): |
| 845 | def __init__(self): |
| 846 | gtk.CellRendererToggle.__init__(self) |
| 847 | self.ctrl = HobCellRendererController(is_draw_row=True) |
| 848 | self.ctrl.running_mode = self.ctrl.MODE_ONE_SHORT |
| 849 | self.cell_attr = {"fadeout": False, "number_of_children": 0} |
| 850 | |
| 851 | def do_render(self, window, widget, background_area, cell_area, expose_area, flags): |
| 852 | if (not self.ctrl) or (not widget): |
| 853 | return |
| 854 | |
| 855 | if flags & gtk.CELL_RENDERER_SELECTED: |
| 856 | state = gtk.STATE_SELECTED |
| 857 | else: |
| 858 | state = gtk.STATE_NORMAL |
| 859 | |
| 860 | if self.ctrl.is_active(): |
| 861 | path = widget.get_path_at_pos(cell_area.x + cell_area.width/2, cell_area.y + cell_area.height/2) |
| 862 | # sometimes the parameters of cell_area will be a negative number,such as pull up down the scroll bar |
| 863 | # it's over the tree container range, so the path will be bad |
| 864 | if not path: return |
| 865 | path = path[0] |
| 866 | if path in self.ctrl.running_cell_areas: |
| 867 | cr = window.cairo_create() |
| 868 | color = widget.get_style().base[state] |
| 869 | |
| 870 | row_x, _, row_width, _ = widget.get_visible_rect() |
| 871 | border_y = self.get_property("ypad") |
| 872 | self.ctrl.on_draw_fadeinout_cb(cr, color, row_x, cell_area.y - border_y, row_width, \ |
| 873 | cell_area.height + border_y * 2, self.cell_attr["fadeout"]) |
| 874 | # draw number of a group |
| 875 | if self.cell_attr["number_of_children"]: |
| 876 | text = "%d pkg" % self.cell_attr["number_of_children"] |
| 877 | pangolayout = widget.create_pango_layout(text) |
| 878 | textw, texth = pangolayout.get_pixel_size() |
| 879 | x = cell_area.x + (cell_area.width/2) - (textw/2) |
| 880 | y = cell_area.y + (cell_area.height/2) - (texth/2) |
| 881 | |
| 882 | widget.style.paint_layout(window, state, True, cell_area, widget, "checkbox", x, y, pangolayout) |
| 883 | else: |
| 884 | return gtk.CellRendererToggle.do_render(self, window, widget, background_area, cell_area, expose_area, flags) |
| 885 | |
| 886 | '''delay: normally delay time is 1000ms |
| 887 | cell_list: whilch cells need to be render |
| 888 | ''' |
| 889 | def fadeout(self, tree, delay, cell_list=None): |
| 890 | if (delay < 200) or (not tree): |
| 891 | return |
| 892 | self.cell_attr["fadeout"] = True |
| 893 | self.ctrl.running_cell_areas = cell_list |
| 894 | self.ctrl.start_run(200, 0, 0, delay, (delay * 200 / 1000), tree) |
| 895 | |
| 896 | def connect_render_state_changed(self, func, usrdata=None): |
| 897 | if not func: |
| 898 | return |
| 899 | if usrdata: |
| 900 | self.ctrl.connect("run-timer-stopped", func, self, usrdata) |
| 901 | else: |
| 902 | self.ctrl.connect("run-timer-stopped", func, self) |
| 903 | |
| 904 | gobject.type_register(HobCellRendererToggle) |