def test_size(self): le = LayoutEngine() le.updateWidth(20) le.updateCells(40, 10, 10) area = Rectangle(0, 0, 20, 20) cells = le.getVisibleCells(area) exp = {0: Rectangle(0, 0, 10, 10), 1: Rectangle(10, 0, 10, 10), 2: Rectangle(0, 10, 10, 10), 3: Rectangle(10, 10, 10, 10)} self.assertEqual(200, le.getHeight()) self.assertDictEqual(exp, cells) le.updateWidth(30) area = Rectangle(0, 1, 13, 20) cells = le.getVisibleCells(area) exp = {0: Rectangle(0, 0, 10, 10), 1: Rectangle(10, 0, 10, 10), 3: Rectangle(0,10, 10, 10), 4: Rectangle(10,10, 10, 10), 6: Rectangle(0,20, 10, 10), 7: Rectangle(10,20, 10, 10)} self.assertEqual(140, le.getHeight()) self.assertDictEqual(cells, exp) cells = le.getVisibleCells(Rectangle(15, 15, 3, 3)) self.assertDictEqual(cells, {4: Rectangle(10, 10, 10, 10)}) # test layout with spacing le.updateWidth(23) cells = le.getVisibleCells(Rectangle(0, 0, 23, 5)) self.assertDictEqual(cells, {0: Rectangle(0, 0, 10, 10), 1: Rectangle(10, 0, 10, 10)}) # test not full layout le.updateCells(3, 10, 20) le.updateWidth(20) cells = le.getVisibleCells(Rectangle(0, 20, 20, 15)) self.assertDictEqual(cells, {2: Rectangle(0, 20, 10, 20)})
def test_size(self): le = LayoutEngine() le.updateWidth(20) le.updateCells(40, 10, 10) area = Rectangle(0, 0, 20, 20) cells = le.getVisibleCells(area) exp = { 0: Rectangle(0, 0, 10, 10), 1: Rectangle(10, 0, 10, 10), 2: Rectangle(0, 10, 10, 10), 3: Rectangle(10, 10, 10, 10) } self.assertEqual(200, le.getHeight()) self.assertDictEqual(exp, cells) le.updateWidth(30) area = Rectangle(0, 1, 13, 20) cells = le.getVisibleCells(area) exp = { 0: Rectangle(0, 0, 10, 10), 1: Rectangle(10, 0, 10, 10), 3: Rectangle(0, 10, 10, 10), 4: Rectangle(10, 10, 10, 10), 6: Rectangle(0, 20, 10, 10), 7: Rectangle(10, 20, 10, 10) } self.assertEqual(140, le.getHeight()) self.assertDictEqual(cells, exp) cells = le.getVisibleCells(Rectangle(15, 15, 3, 3)) self.assertDictEqual(cells, {4: Rectangle(10, 10, 10, 10)}) # test layout with spacing le.updateWidth(23) cells = le.getVisibleCells(Rectangle(0, 0, 23, 5)) self.assertDictEqual(cells, { 0: Rectangle(0, 0, 10, 10), 1: Rectangle(10, 0, 10, 10) }) # test not full layout le.updateCells(3, 10, 20) le.updateWidth(20) cells = le.getVisibleCells(Rectangle(0, 20, 20, 15)) self.assertDictEqual(cells, {2: Rectangle(0, 20, 10, 20)})
class ThumbnailsView(Gtk.Layout): scroll_value = 0 scroll = False real_focus_cell = 0 # initial thumbnail size real_thumbnail_width = 160 thumbnail_height = 160 sort_by = 'date' reversed_sort_order = False priority_loads = [] def __init__(self): Gtk.Layout.__init__(self) self.thumbCache = {} self.layoutEngine = LayoutEngine() self.loading_pixbuf = GdkPixbuf.Pixbuf.new_from_file('data/gfx/refresh.png') self.selection = selection self.thumbnail_pixbufs = {} self.exif_tags = {} self.height = 100 self.width = 100 self.visible = Rectangle(0, 0, 1, 1) self.connect('draw', self.on_draw_event) self.connect('key-press-event', self.on_key_press) self.set_can_focus(True) self.items = [] self.update_layout() selection.add_notify_callback(self.notify_selection_change) self.hdl = None vadj = self.get_vadjustment() hadj = self.get_hadjustment() vadj.connect('value-changed', self.on_adjustment_value_changed) hadj.connect('value-changed', self.on_adjustment_value_changed) Gtk.Scrollable.set_vadjustment(self, vadj) Gtk.Scrollable.set_hadjustment(self, hadj) def __load_thumbnails_bg(self): while True: if self.priority_loads: idx = self.priority_loads.pop(0) self.get_thumb(idx) self.invalidate_cell(idx) time.sleep(0.01) yield True def get_thumb(self, idx): """ create a PixBuf containing the image's thumb and flags images (basket, read-only, raw) the image's full path is used as buffer's key """ node = self.items[idx] if not self.is_thumb(idx): Buffer.images[node.file] = node.getThumb() pb = Buffer.images[node.file] pb2 = 0 if node.isInBasket: if pb2 == 0: pb2 = pb.copy() Buffer.pbBasket.copy_area(0, 0, 15, 13, pb2, 7, 7) if node.isReadOnly: if pb2 == 0: pb2 = pb.copy() wx = pb.get_width() Buffer.pbReadOnly.copy_area(0, 0, 15, 13, pb2, wx - 22, 7) if node.rating: if pb2 == 0: pb2 = pb.copy() i = 0 while i < node.rating: Buffer.pbReadOnly.copy_area(5, 4, 5, 5, pb2, 25 + 7 * i, 11) i += 1 if pb2 != 0: pb = pb2 return pb def stop(self): """ stop background process which load thumbs """ if self.hdl: GObject.source_remove(self.hdl) self.hdl = None def start(self): """ start background process to load thumbs """ if not self.hdl: gen = self.__load_thumbnails_bg() def f(): return next(gen) self.hdl = GObject.idle_add(f) def set_photos(self, photos): self.stop() self.items = photos # ~ self.sort_photos(photos) self.update_layout() self.selection.empty() self.priority_loads = [] self.start() def get_thumbnail_width(self): return self.real_thumbnail_width def set_thumbnail_width(self, value): self.thumbnail_height = value # int(value * 3./4) self.real_thumbnail_width = int(value) self.invalidate_view() self.update_layout() thumbnail_width = property(get_thumbnail_width, set_thumbnail_width) def on_key_press(self, widget, event): shift = event.state & (Gdk.KEY_Shift_L | Gdk.KEY_Shift_R) ctrl = event.state & (Gdk.KEY_Control_L | Gdk.KEY_Control_R) focus_old = self.focus_cell if event.keyval == Gdk.KEY_Down: self.focus_cell += self.cells_per_row elif event.keyval == Gdk.KEY_Left: if ctrl and shift: self.focus_cell -= self.focus_cell % self.cells_per_row else: self.focus_cell -= 1 elif event.keyval == Gdk.KEY_Right: if ctrl and shift: self.focus_cell += self.cells_per_row - \ (self.focus_cell % self.cells_per_row) - 1 else: self.focus_cell += 1 elif event.keyval == Gdk.KEY_Up: self.focus_cell -= self.cells_per_row elif event.keyval == Gdk.KEY_Home: self.focus_cell = 0 elif event.keyval == Gdk.KEY_End: self.focus_cell = len(self.items) - 1 elif ctrl and event.keyval == ord('a'): # select All self.selection.set(range(0, len(self.items))) else: return False self.focus_cell = max(self.focus_cell, 0) self.focus_cell = min(self.focus_cell, len(self.items) - 1) if self.focus_cell == focus_old: # so up from the first or down from the last doesn't tab # to the prev/next widget return True self.selection.freeze() if shift: if focus_old != self.focus_cell and focus_old in self.selection \ and self.focus_cell in self.selection: for i in range(min(focus_old, self.focus_cell) + 1, max(focus_old, self.focus_cell) + 1): if i in self.selection: self.selection.remove(i) else: for i in range(min(focus_old, self.focus_cell), max(focus_old, self.focus_cell) + 1): if not i in self.selection: self.selection.append(i) else: self.selection.set([self.focus_cell]) self.selection.thaw() self.scroll_to(self.focus_cell) return True def on_adjustment_value_changed(self, adj, *args): self.visible.y = int(adj.get_value()) self.do_scroll() def onHAdjustmentValueChanged(self, adj, *args): self.do_scroll() def do_scroll(self): pass def update_layout(self, rectangle=None): if rectangle is None: rectangle = self.get_allocation() self.layoutEngine.updateWidth(rectangle.width) self.layoutEngine.updateCells(len(self.items), self.thumbnail_width, self.thumbnail_height) self.height = self.layoutEngine.getHeight() self.visible.width = rectangle.width self.visible.height = rectangle.height vadjustment = self.get_vadjustment() hadjustment = self.get_hadjustment() vadjustment.step_increment = self.thumbnail_height vadjustment.connect('value-changed', self.on_adjustment_value_changed) x = hadjustment.get_value() y = self.height * self.scroll_value self.set_size(x, y, self.visible.width, self.height) def set_size(self, x, y, width, height): vadjustment = self.get_vadjustment() hadjustment = self.get_hadjustment() xchange = False ychange = False hadjustment.upper = max(self.get_allocation().width, width) vadjustment.upper = max(self.get_allocation().height, height) if self.scroll: xchange = (hadjustment.value != x) ychange = (vadjustment.value != y) self.scroll = False if self.get_realized(): self.get_bin_window().freeze_updates() #self.bin_window.freeze_updates() if xchange or ychange: if self.get_realized(): self.bin_window.move_resize(-x, -y, hadjustment.upper, vadjustment.upper) vadjustment.value = y hadjustment.value = x if self.scroll: self.scroll = False if width != self.get_allocation().width or height != self.get_allocation().height: Gtk.Layout.set_size(self, width, height) if xchange or ychange: vadjustment.change_value() hadjustment.change_value() if self.get_realized(): self.get_bin_window().thaw_updates() self.get_bin_window().process_updates(True) def on_draw_event(self, widget, context): alloc = self.get_allocation() area = [alloc.x, alloc.y, self.get_allocated_width(), self.get_allocated_height()] # context.set_source_rgb(0, 0, 0) # context.set_line_width(0.5) # context.rectangle(10, 10, 10, 20) # # import random # mx = min(alloc.width, alloc.height) # self.coords = [] # for _ in range(30): # self.coords.append((random.randint(0, mx), random.randint(0, mx))) # for i in self.coords: # for j in self.coords: # context.move_to(i[0], i[1]) # context.line_to(j[0], j[1]) # context.stroke() print(self.visible) self.draw_all_cells(self.visible, context) context.stroke() return False def get_cell_position(self, cell_num): if self.cells_per_row == 0: return 0, 0 row, col = divmod(cell_num, self.cells_per_row) x = col * self.cell_width + BORDER_SIZE y = row * self.cell_height + BORDER_SIZE return x, y def draw_all_cells(self, area, cr): cells = self.layoutEngine.getVisibleCells(area) for cellNum, cell in cells.items(): self.drawCell(cr, cell, cellNum) def drawCell(self, cr, cell, cellNum): img = self.items[cellNum] fname = img.file thumb = self.thumbCache.get(fname) print('draw thumb: ', fname ) if not thumb: thumb = GdkPixbuf.Pixbuf.new_from_file(fname) thumb = thumb.scale_simple(self.thumbnail_width, self.thumbnail_height, GdkPixbuf.InterpType.BILINEAR) self.thumbCache[fname] = thumb x = cell.x - self.visible.x y = cell.y - self.visible.y Gdk.cairo_set_source_pixbuf(cr, thumb, x, y) cr.paint() cr.stroke() # cr.rectangle(cell.x - self.visible.x, cell.y - self.visible.y, cell.width, cell.height) # self.get_thumbnail_pixbuf(cellNum) # cr.stroke() def cell_bounds(self, cell, cr): x, y = self.get_cell_position(cell) return [x, y, self.cell_width, self.cell_height] def get_thumbnail_pixbuf(self, thumbnail_num): if not self.is_thumb(thumbnail_num): if thumbnail_num in self.priority_loads: self.priority_loads.remove(thumbnail_num) self.priority_loads.insert(0, thumbnail_num) pixbuf = self.loading_pixbuf else: pixbuf = self.get_thumb(thumbnail_num) if pixbuf.get_width() > pixbuf.get_height(): wx, wy = self.thumbnail_width, self.thumbnail_height else: r = float(pixbuf.get_height()) / self.thumbnail_height wx, wy = int(pixbuf.get_width() / r), self.thumbnail_height # ~ pixbuf = pixbuf.scale_simple(wx, wy, gtk.gdk.INTERP_BILINEAR) pixbuf = pixbuf.scale_simple(wx, wy, gtk.gdk.INTERP_NEAREST) return pixbuf def draw_cell(self, thumbnail_num, area, cr): bounds = self.cell_bounds(thumbnail_num, cr) ins = rectIntersect(bounds, area) if not rectIntersect(bounds, area): return # a = GdkPixbuf.Pixbuf.new_from_file('/media/data/ph/marn2/DSC_0647.JPG') # a = a.scale_simple(150, 150, GdkPixbuf.InterpType.BILINEAR) # thumbnail = a.scale_simple(self.cell_width, self.cell_height, GdkPixbuf.InterpType.BILINEAR) # Gdk.cairo_set_source_pixbuf(cr, a, a.get_width(), a.get_height()) # cr.paint() # cr.stroke() thumbnail = self.get_thumbnail_pixbuf(thumbnail_num) selected = thumbnail_num in self.selection if selected: if self.has_focus(): cell_state = Gtk.StateFlags.SELECTED else: cell_state = Gtk.StateFlags.ACTIVE else: cell_state = Gtk.StateFlags.NORMAL if thumbnail != self.loading_pixbuf: cr.rectangle(bounds[0], bounds[1], bounds[2] - 1, bounds[3] - 1) # self.style.paint_flat_box(self.bin_window, cell_state, # gtk.SHADOW_OUT, area, self, # 'ThumbnailsView', # bounds.x, bounds.y, # bounds.width - 1, bounds.height - 1) def inflate(rect, x, y): return (rect[0] - x, rect[1] - y, rect[2] + 2*x, rect[3]+2*y) #isFocused = False if self.has_focus() and thumbnail_num == self.focus_cell: focus = inflate(bounds, -3, -3) # self.get_style().paint_focus(self.bin_window, cell_state, # area, self, None, focus.x, focus.y, # focus.width, focus.height) #isFocused = True region = (0, 0, 0, 0) image_bounds = inflate(bounds, -CELL_BORDER_WIDTH, -CELL_BORDER_WIDTH) if selected: expansion = SELECTION_THICKNESS else: expansion = 0 boundsInflated = inflate(image_bounds, expansion + 1, expansion + 1) image_bounds = rectIntersect(image_bounds, area) if image_bounds[2]: def fit(orig_width, orig_height, dest_width, dest_height): if orig_width == 0 or orig_height == 0: return 0, 0 scale = min(dest_width / orig_width, dest_height / orig_height) if scale > 1: scale = 1 fit_width = scale * orig_width fit_height = scale * orig_height return fit_width, fit_height # resizing during the painting (pn.getThumb give some 160x160) w, h = fit(thumbnail.get_width(), thumbnail.get_height(), self.thumbnail_width, self.thumbnail_width) # resizing during the extraction (pn.getThumb desired size) # w, h = fit(thumbnail.get_width(), thumbnail.get_height(), # 160, 160) cr.set_line_width(1) cr.set_source_rgb(0, 0, 0) cr.rectangle(10, 10, 10, 10) region = cr.rectangle(bounds[0], bounds[1], bounds[2], bounds[3]) region = bounds # bounds.x + (bounds.width - w) / 2, # bounds.y + self.thumbnail_height - h + CELL_BORDER_WIDTH, # w, h) # EXPAND WHEN SELECTED ! region = inflate(region, expansion, expansion) region = (region[0], region[1], max(1, region[2]), max(1, region[3])) #a = GdkPixbuf.Pixbuf.new_from_file('/media/data/ph/marn2/DSC_0647.JPG') if region[2] != thumbnail.get_width() and region[3] != thumbnail.get_height(): # the speedest temp_thumbnail = thumbnail.scale_simple(region[2], region[3], GdkPixbuf.InterpType.BILINEAR) else: temp_thumbnail = thumbnail region = (region[0], region[1], temp_thumbnail.get_width(), temp_thumbnail.get_height()) draw = inflate(region, 1, 1) # if thumbnail != self.loading_pixbuf: # self.style.paint_shadow(self.bin_window, cell_state, # gtk.SHADOW_OUT, area, self, # 'ThumbnailsView', draw.x, # draw.y, draw.width, draw.height) draw = rectIntersect(region, area) if draw[2]: cr.rectangle(draw[0], draw[1], draw[2], draw[3]) # Gdk.cairo_set_source_pixbuf(cr, a, draw[0], draw[1]) # cr.paint() # cr.stroke() # self.get_bin_window().draw_pixbuf( # self.style.white_gc, # temp_thumbnail, # draw.x - region.x, # draw.y - region.y, # draw.x, draw.y, # draw.width, draw.height, # gtk.gdk.RGB_DITHER_NONE, # draw.x, draw.y) layout_bounds = (0, 0, 0, 0) item = self.items[thumbnail_num] if item: layout = Pango.Layout(self.get_pango_context()) layout.set_font_description(self.get_style().font_desc) t = self.get_text(thumbnail_num) layout.set_text(t, len(t)) # ~ if isFocused: layout.set_width((region[2] + 6) * 1000) layout.set_wrap(1) layout_bounds = (layout_bounds[0], layout_bounds[1]) + layout.get_pixel_size() lbX = bounds[0] + (bounds[2] - layout_bounds[2]) / 2 lbY = bounds[1] + bounds[3] - CELL_BORDER_WIDTH - layout_bounds[3] + 3 layout_bounds = (lbX, lbY) + layout_bounds[2:] region = rectIntersect(layout_bounds, area) if region is None: region = (0, 0, 0, 0) if region[2]: cr.rectangle(layout_bounds[0], layout_bounds[1], layout_bounds[2], layout_bounds[3]) # self.style.paint_flat_box(self.bin_window, cell_state, # gtk.SHADOW_OUT, area, self, # 'ThumbnailsView', # layout_bounds.x, # layout_bounds.y, # layout_bounds.width, # layout_bounds.height) # # self.style.paint_layout(self.bin_window, cell_state, # True, area, self, # 'ThumbnailsView', # layout_bounds.x, # layout_bounds.y, layout) def scroll_to(self, cell_num, center=True): if not self.get_realized(): return adjustment = self.get_vadjustment() x, y = self.get_cell_position(cell_num) if y + self.cell_height > adjustment.upper: self.update_layout() if center: t = y + self.cell_height / 2 - adjustment.page_size / 2 if t < 0: t = 0 elif t + adjustment.page_size > adjustment.upper: t = adjustment.upper - adjustment.page_size adjustment.value = t else: adjustment.value = y
class ThumbView(QWidget): needThumb = pyqtSignal(int, str) def __init__(self): self.counter = 0 super().__init__() self.thumbWidth = 100 self.thumbHeight = 80 self.thumbs = {} self.items = [] self.cellsInView = {} self.noThumbPixmap = QPixmap('data/gfx/noThumb.png') self.noThumbPixmap = self.resizeImage( self.noThumbPixmap, (self.thumbWidth, self.thumbHeight)) self.canvas = GScrollArea() self.canvas.setResizeEventCallback(self.canvasResizeEvent) self.w = QWidget() self.w.paintEvent = self.canvasPaintEvent self.canvas.setWidget(self.w) self.canvas.setSizePolicy( QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)) label = QLabel('Thumbnails view') layout = QGridLayout() layout.addWidget(label, 0, 0, 1, 1) layout.addWidget(self.canvas, 1, 0, 1, 1) self.setLayout(layout) self.cnt = 0 self.width = 500 self.layoutEngine = LayoutEngine() self.layoutEngine.updateCells(0, 100, 100) self.layoutEngine.updateWidth(self.width) self.setCanvasSize(self.width, self.layoutEngine.getHeight()) def canvasResizeEvent(self, ev: QResizeEvent): oldSize = ev.oldSize() newSize = ev.size() if oldSize.width() != newSize.width(): self.updateLayout(newSize.width()) def setItems(self, items): self.items = items nCells = len(items) print('Set items: ', nCells) self.layoutEngine.updateCells(nCells, 100, 100) self.updateLayout(self.layoutEngine.getWidth()) self.w.repaint() def updateLayout(self, width): self.layoutEngine.updateWidth(width - 1) height = self.layoutEngine.getHeight() self.setCanvasSize(width, height) def repaintCanvas(self): self.w.repaint() def setCanvasSize(self, w, h): self.w.resize(w, h) def canvasPaintEvent(self, ev: QPaintEvent): print(self.counter, ' ', end='') self.counter += 1 repaintRect = ev.rect() repaintArea = Rectangle(repaintRect.x(), repaintRect.y(), repaintRect.width(), repaintRect.height()) painter = QPainter(self.w) thumbRequested = False cells = self.layoutEngine.getVisibleCells(repaintArea) for cellNum, cell in cells.items(): thumb = self.items[cellNum] if thumb.getPath() in self.thumbs: self.drawThumnail(cellNum, self.thumbs[thumb.getPath()], thumb, cell, painter) else: if not thumbRequested: self.needThumb.emit(cellNum, thumb.getPath()) thumbRequested = True self.drawThumnail(cellNum, self.noThumbPixmap, self.items[cellNum], cell, painter) def drawThumnail(self, cellNum, pic, thumb: PhotoNode, cell: Rectangle, painter: QPainter): rect = QRect(cell.x, cell.y, cell.width, cell.height) bLeft = rect.bottomLeft() # draw name fontHeight = 20 font = painter.font() font.setPixelSize(fontHeight) textTopRight = QPoint(bLeft.x(), bLeft.y() - fontHeight) textRect = QRect(textTopRight, rect.bottomRight()) thumbName = thumb.name painter.drawText(textRect, Qt.AlignHCenter, thumbName) #painter.drawRect(rect) # draw thumb imageRect = QRect(rect.topLeft(), textRect.topRight()) imageCenter = imageRect.center() imageX = int(imageCenter.x() - pic.width() / 2) imageY = int(imageCenter.y() - pic.height() / 2) imageOrigin = QPoint(imageX, imageY) painter.drawPixmap(imageOrigin, pic) @pyqtSlot(int, str, Image.Image) def updateThumb(self, thumbId: int, path: str, pic: Image.Image): thumb = ImageQt.toqimage(pic) thumb = self.resizeImage(thumb, (self.thumbWidth, self.thumbHeight)) thumb = QPixmap.fromImage(thumb) self.thumbs[path] = thumb self.repaintCanvas() def resizeImage(self, img: QImage, size: tuple): x = img.width() y = img.height() if x <= size[0] and y <= size[1]: return img origK = img.height() / img.width() if x > size[0]: x = size[0] y = int(origK * x) if y > size[1]: y = size[1] x = (y / origK) return img.scaled(x, y, transformMode=QtCore.Qt.SmoothTransformation)
class ThumbnailsView(Gtk.Layout): scroll_value = 0 scroll = False real_focus_cell = 0 # initial thumbnail size real_thumbnail_width = 160 thumbnail_height = 160 sort_by = 'date' reversed_sort_order = False priority_loads = [] def __init__(self): Gtk.Layout.__init__(self) self.thumbCache = {} self.layoutEngine = LayoutEngine() self.loading_pixbuf = GdkPixbuf.Pixbuf.new_from_file( 'data/gfx/refresh.png') self.selection = selection self.thumbnail_pixbufs = {} self.exif_tags = {} self.height = 100 self.width = 100 self.visible = Rectangle(0, 0, 1, 1) self.connect('draw', self.on_draw_event) self.connect('key-press-event', self.on_key_press) self.set_can_focus(True) self.items = [] self.update_layout() selection.add_notify_callback(self.notify_selection_change) self.hdl = None vadj = self.get_vadjustment() hadj = self.get_hadjustment() vadj.connect('value-changed', self.on_adjustment_value_changed) hadj.connect('value-changed', self.on_adjustment_value_changed) Gtk.Scrollable.set_vadjustment(self, vadj) Gtk.Scrollable.set_hadjustment(self, hadj) def __load_thumbnails_bg(self): while True: if self.priority_loads: idx = self.priority_loads.pop(0) self.get_thumb(idx) self.invalidate_cell(idx) time.sleep(0.01) yield True def get_thumb(self, idx): """ create a PixBuf containing the image's thumb and flags images (basket, read-only, raw) the image's full path is used as buffer's key """ node = self.items[idx] if not self.is_thumb(idx): Buffer.images[node.file] = node.getThumb() pb = Buffer.images[node.file] pb2 = 0 if node.isInBasket: if pb2 == 0: pb2 = pb.copy() Buffer.pbBasket.copy_area(0, 0, 15, 13, pb2, 7, 7) if node.isReadOnly: if pb2 == 0: pb2 = pb.copy() wx = pb.get_width() Buffer.pbReadOnly.copy_area(0, 0, 15, 13, pb2, wx - 22, 7) if node.rating: if pb2 == 0: pb2 = pb.copy() i = 0 while i < node.rating: Buffer.pbReadOnly.copy_area(5, 4, 5, 5, pb2, 25 + 7 * i, 11) i += 1 if pb2 != 0: pb = pb2 return pb def stop(self): """ stop background process which load thumbs """ if self.hdl: GObject.source_remove(self.hdl) self.hdl = None def start(self): """ start background process to load thumbs """ if not self.hdl: gen = self.__load_thumbnails_bg() def f(): return next(gen) self.hdl = GObject.idle_add(f) def set_photos(self, photos): self.stop() self.items = photos # ~ self.sort_photos(photos) self.update_layout() self.selection.empty() self.priority_loads = [] self.start() def get_thumbnail_width(self): return self.real_thumbnail_width def set_thumbnail_width(self, value): self.thumbnail_height = value # int(value * 3./4) self.real_thumbnail_width = int(value) self.invalidate_view() self.update_layout() thumbnail_width = property(get_thumbnail_width, set_thumbnail_width) def on_key_press(self, widget, event): shift = event.state & (Gdk.KEY_Shift_L | Gdk.KEY_Shift_R) ctrl = event.state & (Gdk.KEY_Control_L | Gdk.KEY_Control_R) focus_old = self.focus_cell if event.keyval == Gdk.KEY_Down: self.focus_cell += self.cells_per_row elif event.keyval == Gdk.KEY_Left: if ctrl and shift: self.focus_cell -= self.focus_cell % self.cells_per_row else: self.focus_cell -= 1 elif event.keyval == Gdk.KEY_Right: if ctrl and shift: self.focus_cell += self.cells_per_row - \ (self.focus_cell % self.cells_per_row) - 1 else: self.focus_cell += 1 elif event.keyval == Gdk.KEY_Up: self.focus_cell -= self.cells_per_row elif event.keyval == Gdk.KEY_Home: self.focus_cell = 0 elif event.keyval == Gdk.KEY_End: self.focus_cell = len(self.items) - 1 elif ctrl and event.keyval == ord('a'): # select All self.selection.set(range(0, len(self.items))) else: return False self.focus_cell = max(self.focus_cell, 0) self.focus_cell = min(self.focus_cell, len(self.items) - 1) if self.focus_cell == focus_old: # so up from the first or down from the last doesn't tab # to the prev/next widget return True self.selection.freeze() if shift: if focus_old != self.focus_cell and focus_old in self.selection \ and self.focus_cell in self.selection: for i in range( min(focus_old, self.focus_cell) + 1, max(focus_old, self.focus_cell) + 1): if i in self.selection: self.selection.remove(i) else: for i in range(min(focus_old, self.focus_cell), max(focus_old, self.focus_cell) + 1): if not i in self.selection: self.selection.append(i) else: self.selection.set([self.focus_cell]) self.selection.thaw() self.scroll_to(self.focus_cell) return True def on_adjustment_value_changed(self, adj, *args): self.visible.y = int(adj.get_value()) self.do_scroll() def onHAdjustmentValueChanged(self, adj, *args): self.do_scroll() def do_scroll(self): pass def update_layout(self, rectangle=None): if rectangle is None: rectangle = self.get_allocation() self.layoutEngine.updateWidth(rectangle.width) self.layoutEngine.updateCells(len(self.items), self.thumbnail_width, self.thumbnail_height) self.height = self.layoutEngine.getHeight() self.visible.width = rectangle.width self.visible.height = rectangle.height vadjustment = self.get_vadjustment() hadjustment = self.get_hadjustment() vadjustment.step_increment = self.thumbnail_height vadjustment.connect('value-changed', self.on_adjustment_value_changed) x = hadjustment.get_value() y = self.height * self.scroll_value self.set_size(x, y, self.visible.width, self.height) def set_size(self, x, y, width, height): vadjustment = self.get_vadjustment() hadjustment = self.get_hadjustment() xchange = False ychange = False hadjustment.upper = max(self.get_allocation().width, width) vadjustment.upper = max(self.get_allocation().height, height) if self.scroll: xchange = (hadjustment.value != x) ychange = (vadjustment.value != y) self.scroll = False if self.get_realized(): self.get_bin_window().freeze_updates() #self.bin_window.freeze_updates() if xchange or ychange: if self.get_realized(): self.bin_window.move_resize(-x, -y, hadjustment.upper, vadjustment.upper) vadjustment.value = y hadjustment.value = x if self.scroll: self.scroll = False if width != self.get_allocation( ).width or height != self.get_allocation().height: Gtk.Layout.set_size(self, width, height) if xchange or ychange: vadjustment.change_value() hadjustment.change_value() if self.get_realized(): self.get_bin_window().thaw_updates() self.get_bin_window().process_updates(True) def on_draw_event(self, widget, context): alloc = self.get_allocation() area = [ alloc.x, alloc.y, self.get_allocated_width(), self.get_allocated_height() ] # context.set_source_rgb(0, 0, 0) # context.set_line_width(0.5) # context.rectangle(10, 10, 10, 20) # # import random # mx = min(alloc.width, alloc.height) # self.coords = [] # for _ in range(30): # self.coords.append((random.randint(0, mx), random.randint(0, mx))) # for i in self.coords: # for j in self.coords: # context.move_to(i[0], i[1]) # context.line_to(j[0], j[1]) # context.stroke() print(self.visible) self.draw_all_cells(self.visible, context) context.stroke() return False def get_cell_position(self, cell_num): if self.cells_per_row == 0: return 0, 0 row, col = divmod(cell_num, self.cells_per_row) x = col * self.cell_width + BORDER_SIZE y = row * self.cell_height + BORDER_SIZE return x, y def draw_all_cells(self, area, cr): cells = self.layoutEngine.getVisibleCells(area) for cellNum, cell in cells.items(): self.drawCell(cr, cell, cellNum) def drawCell(self, cr, cell, cellNum): img = self.items[cellNum] fname = img.file thumb = self.thumbCache.get(fname) print('draw thumb: ', fname) if not thumb: thumb = GdkPixbuf.Pixbuf.new_from_file(fname) thumb = thumb.scale_simple(self.thumbnail_width, self.thumbnail_height, GdkPixbuf.InterpType.BILINEAR) self.thumbCache[fname] = thumb x = cell.x - self.visible.x y = cell.y - self.visible.y Gdk.cairo_set_source_pixbuf(cr, thumb, x, y) cr.paint() cr.stroke() # cr.rectangle(cell.x - self.visible.x, cell.y - self.visible.y, cell.width, cell.height) # self.get_thumbnail_pixbuf(cellNum) # cr.stroke() def cell_bounds(self, cell, cr): x, y = self.get_cell_position(cell) return [x, y, self.cell_width, self.cell_height] def get_thumbnail_pixbuf(self, thumbnail_num): if not self.is_thumb(thumbnail_num): if thumbnail_num in self.priority_loads: self.priority_loads.remove(thumbnail_num) self.priority_loads.insert(0, thumbnail_num) pixbuf = self.loading_pixbuf else: pixbuf = self.get_thumb(thumbnail_num) if pixbuf.get_width() > pixbuf.get_height(): wx, wy = self.thumbnail_width, self.thumbnail_height else: r = float(pixbuf.get_height()) / self.thumbnail_height wx, wy = int(pixbuf.get_width() / r), self.thumbnail_height # ~ pixbuf = pixbuf.scale_simple(wx, wy, gtk.gdk.INTERP_BILINEAR) pixbuf = pixbuf.scale_simple(wx, wy, gtk.gdk.INTERP_NEAREST) return pixbuf def draw_cell(self, thumbnail_num, area, cr): bounds = self.cell_bounds(thumbnail_num, cr) ins = rectIntersect(bounds, area) if not rectIntersect(bounds, area): return # a = GdkPixbuf.Pixbuf.new_from_file('/media/data/ph/marn2/DSC_0647.JPG') # a = a.scale_simple(150, 150, GdkPixbuf.InterpType.BILINEAR) # thumbnail = a.scale_simple(self.cell_width, self.cell_height, GdkPixbuf.InterpType.BILINEAR) # Gdk.cairo_set_source_pixbuf(cr, a, a.get_width(), a.get_height()) # cr.paint() # cr.stroke() thumbnail = self.get_thumbnail_pixbuf(thumbnail_num) selected = thumbnail_num in self.selection if selected: if self.has_focus(): cell_state = Gtk.StateFlags.SELECTED else: cell_state = Gtk.StateFlags.ACTIVE else: cell_state = Gtk.StateFlags.NORMAL if thumbnail != self.loading_pixbuf: cr.rectangle(bounds[0], bounds[1], bounds[2] - 1, bounds[3] - 1) # self.style.paint_flat_box(self.bin_window, cell_state, # gtk.SHADOW_OUT, area, self, # 'ThumbnailsView', # bounds.x, bounds.y, # bounds.width - 1, bounds.height - 1) def inflate(rect, x, y): return (rect[0] - x, rect[1] - y, rect[2] + 2 * x, rect[3] + 2 * y) #isFocused = False if self.has_focus() and thumbnail_num == self.focus_cell: focus = inflate(bounds, -3, -3) # self.get_style().paint_focus(self.bin_window, cell_state, # area, self, None, focus.x, focus.y, # focus.width, focus.height) #isFocused = True region = (0, 0, 0, 0) image_bounds = inflate(bounds, -CELL_BORDER_WIDTH, -CELL_BORDER_WIDTH) if selected: expansion = SELECTION_THICKNESS else: expansion = 0 boundsInflated = inflate(image_bounds, expansion + 1, expansion + 1) image_bounds = rectIntersect(image_bounds, area) if image_bounds[2]: def fit(orig_width, orig_height, dest_width, dest_height): if orig_width == 0 or orig_height == 0: return 0, 0 scale = min(dest_width / orig_width, dest_height / orig_height) if scale > 1: scale = 1 fit_width = scale * orig_width fit_height = scale * orig_height return fit_width, fit_height # resizing during the painting (pn.getThumb give some 160x160) w, h = fit(thumbnail.get_width(), thumbnail.get_height(), self.thumbnail_width, self.thumbnail_width) # resizing during the extraction (pn.getThumb desired size) # w, h = fit(thumbnail.get_width(), thumbnail.get_height(), # 160, 160) cr.set_line_width(1) cr.set_source_rgb(0, 0, 0) cr.rectangle(10, 10, 10, 10) region = cr.rectangle(bounds[0], bounds[1], bounds[2], bounds[3]) region = bounds # bounds.x + (bounds.width - w) / 2, # bounds.y + self.thumbnail_height - h + CELL_BORDER_WIDTH, # w, h) # EXPAND WHEN SELECTED ! region = inflate(region, expansion, expansion) region = (region[0], region[1], max(1, region[2]), max(1, region[3])) #a = GdkPixbuf.Pixbuf.new_from_file('/media/data/ph/marn2/DSC_0647.JPG') if region[2] != thumbnail.get_width( ) and region[3] != thumbnail.get_height(): # the speedest temp_thumbnail = thumbnail.scale_simple( region[2], region[3], GdkPixbuf.InterpType.BILINEAR) else: temp_thumbnail = thumbnail region = (region[0], region[1], temp_thumbnail.get_width(), temp_thumbnail.get_height()) draw = inflate(region, 1, 1) # if thumbnail != self.loading_pixbuf: # self.style.paint_shadow(self.bin_window, cell_state, # gtk.SHADOW_OUT, area, self, # 'ThumbnailsView', draw.x, # draw.y, draw.width, draw.height) draw = rectIntersect(region, area) if draw[2]: cr.rectangle(draw[0], draw[1], draw[2], draw[3]) # Gdk.cairo_set_source_pixbuf(cr, a, draw[0], draw[1]) # cr.paint() # cr.stroke() # self.get_bin_window().draw_pixbuf( # self.style.white_gc, # temp_thumbnail, # draw.x - region.x, # draw.y - region.y, # draw.x, draw.y, # draw.width, draw.height, # gtk.gdk.RGB_DITHER_NONE, # draw.x, draw.y) layout_bounds = (0, 0, 0, 0) item = self.items[thumbnail_num] if item: layout = Pango.Layout(self.get_pango_context()) layout.set_font_description(self.get_style().font_desc) t = self.get_text(thumbnail_num) layout.set_text(t, len(t)) # ~ if isFocused: layout.set_width((region[2] + 6) * 1000) layout.set_wrap(1) layout_bounds = (layout_bounds[0], layout_bounds[1]) + layout.get_pixel_size() lbX = bounds[0] + (bounds[2] - layout_bounds[2]) / 2 lbY = bounds[1] + bounds[3] - CELL_BORDER_WIDTH - layout_bounds[ 3] + 3 layout_bounds = (lbX, lbY) + layout_bounds[2:] region = rectIntersect(layout_bounds, area) if region is None: region = (0, 0, 0, 0) if region[2]: cr.rectangle(layout_bounds[0], layout_bounds[1], layout_bounds[2], layout_bounds[3]) # self.style.paint_flat_box(self.bin_window, cell_state, # gtk.SHADOW_OUT, area, self, # 'ThumbnailsView', # layout_bounds.x, # layout_bounds.y, # layout_bounds.width, # layout_bounds.height) # # self.style.paint_layout(self.bin_window, cell_state, # True, area, self, # 'ThumbnailsView', # layout_bounds.x, # layout_bounds.y, layout) def scroll_to(self, cell_num, center=True): if not self.get_realized(): return adjustment = self.get_vadjustment() x, y = self.get_cell_position(cell_num) if y + self.cell_height > adjustment.upper: self.update_layout() if center: t = y + self.cell_height / 2 - adjustment.page_size / 2 if t < 0: t = 0 elif t + adjustment.page_size > adjustment.upper: t = adjustment.upper - adjustment.page_size adjustment.value = t else: adjustment.value = y
class ThumbView(QWidget): needThumb = pyqtSignal(int, str) def __init__(self): self.counter = 0 super().__init__() self.thumbWidth = 100 self.thumbHeight = 80 self.thumbs = {} self.items = [] self.cellsInView = {} self.noThumbPixmap = QPixmap('data/gfx/noThumb.png') self.noThumbPixmap = self.resizeImage(self.noThumbPixmap, (self.thumbWidth, self.thumbHeight)) self.canvas = GScrollArea() self.canvas.setResizeEventCallback(self.canvasResizeEvent) self.w = QWidget() self.w.paintEvent = self.canvasPaintEvent self.canvas.setWidget(self.w) self.canvas.setSizePolicy(QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)) label = QLabel('Thumbnails view') layout = QGridLayout() layout.addWidget(label, 0, 0, 1, 1) layout.addWidget(self.canvas, 1, 0, 1, 1) self.setLayout(layout) self.cnt = 0 self.width = 500 self.layoutEngine = LayoutEngine() self.layoutEngine.updateCells(0, 100, 100) self.layoutEngine.updateWidth(self.width) self.setCanvasSize(self.width, self.layoutEngine.getHeight()) def canvasResizeEvent(self, ev : QResizeEvent): oldSize = ev.oldSize() newSize = ev.size() if oldSize.width() != newSize.width(): self.updateLayout(newSize.width()) def setItems(self, items): self.items = items nCells = len(items) print('Set items: ', nCells) self.layoutEngine.updateCells(nCells, 100, 100) self.updateLayout(self.layoutEngine.getWidth()) self.w.repaint() def updateLayout(self, width): self.layoutEngine.updateWidth(width - 1) height = self.layoutEngine.getHeight() self.setCanvasSize(width, height) def repaintCanvas(self): self.w.repaint() def setCanvasSize(self, w, h): self.w.resize(w, h) def canvasPaintEvent(self, ev : QPaintEvent): print(self.counter, ' ', end='') self.counter += 1 repaintRect = ev.rect() repaintArea = Rectangle(repaintRect.x(), repaintRect.y(), repaintRect.width(), repaintRect.height()) painter = QPainter(self.w) thumbRequested = False cells = self.layoutEngine.getVisibleCells(repaintArea) for cellNum, cell in cells.items(): thumb = self.items[cellNum] if thumb.getPath() in self.thumbs: self.drawThumnail(cellNum, self.thumbs[thumb.getPath()], thumb, cell, painter) else: if not thumbRequested: self.needThumb.emit(cellNum, thumb.getPath()) thumbRequested = True self.drawThumnail(cellNum, self.noThumbPixmap, self.items[cellNum], cell, painter) def drawThumnail(self, cellNum, pic, thumb : PhotoNode, cell : Rectangle, painter : QPainter): rect = QRect(cell.x, cell.y, cell.width, cell.height) bLeft = rect.bottomLeft() # draw name fontHeight = 20 font = painter.font() font.setPixelSize(fontHeight) textTopRight = QPoint(bLeft.x(), bLeft.y() - fontHeight) textRect = QRect(textTopRight, rect.bottomRight()) thumbName = thumb.name painter.drawText(textRect, Qt.AlignHCenter, thumbName) #painter.drawRect(rect) # draw thumb imageRect = QRect(rect.topLeft(), textRect.topRight()) imageCenter = imageRect.center() imageX = int(imageCenter.x() - pic.width() / 2) imageY = int(imageCenter.y() - pic.height() / 2) imageOrigin = QPoint(imageX, imageY) painter.drawPixmap(imageOrigin, pic) @pyqtSlot(int, str, Image.Image) def updateThumb(self, thumbId : int, path : str, pic : Image.Image): thumb = ImageQt.toqimage(pic) thumb = self.resizeImage(thumb, (self.thumbWidth, self.thumbHeight)) thumb = QPixmap.fromImage(thumb) self.thumbs[path] = thumb self.repaintCanvas() def resizeImage(self, img : QImage, size : tuple): x = img.width() y = img.height() if x <= size[0] and y <= size[1]: return img origK = img.height() / img.width() if x > size[0]: x = size[0] y = int(origK * x) if y > size[1]: y = size[1] x = (y / origK) return img.scaled(x, y, transformMode=QtCore.Qt.SmoothTransformation)