from kivy.app import App from kivy.uix.screenmanager import Screen from kivy.uix.boxlayout import BoxLayout from kivy.uix.floatlayout import FloatLayout from kivy.uix.scrollview import ScrollView from kivy.uix.button import Button from kivy.metrics import dp from kivy.core.window import Window from kivy.effects.scroll import ScrollEffect from kivy.uix.modalview import ModalView from kivy.uix.gridlayout import GridLayout from kivy.uix.textinput import TextInput from kivy.uix.label import Label from kivy.uix.button import Button from kivy.metrics import dp from kivy.clock import Clock # IMPORT TVÝCH DAT from data import Cenik, CenPol, CenPolCreate, Cena, Position # ========================================================= # DUMMY CENÍK # ========================================================= def create_dummy_cenik(): return Cenik(cenpol=[ CenPol( id=1, id_card=100, d_name="Pivo", ch_name="Pivo", pokl="A", sklad="A", ceny=[Cena(cena=50, mena="CZK", dan="21", name="standard", cena2=25, cena3=17, cena4=12)], pos_pc=[Position(page=1, line=0, col=0)], ), CenPol( id=2, id_card=101, d_name="Káva", ch_name="Káva", pokl="A", sklad="A", ceny=[Cena(cena=60, mena="CZK", dan="21", name="standard", cena2=30, cena3=20, cena4=15)], pos_pc=[Position(page=1, line=1, col=0)], ), ]) # ========================================================= # EDITOR SCREEN # ========================================================= class WidthDialog(ModalView): def __init__(self, on_ok, default=3, **kwargs): super().__init__(**kwargs) self.on_ok = on_ok self.size_hint = (None, None) self.size = (dp(300), dp(200)) self.auto_dismiss = False root = BoxLayout(orientation="vertical", spacing=dp(10), padding=dp(10)) self.input = TextInput( text=str(default), multiline=False, input_filter="int" ) btn_ok = Button(text="OK") btn_cancel = Button(text="Zrušit") btn_ok.bind(on_press=self._ok) btn_cancel.bind(on_press=lambda *_: self.dismiss()) root.add_widget(self.input) root.add_widget(btn_ok) root.add_widget(btn_cancel) self.add_widget(root) def _ok(self, *_): try: val = int(self.input.text) if val < 1: val = 1 except: val = 3 if self.on_ok: self.on_ok(val) self.dismiss() class CenikItemEditor(ModalView): def __init__(self, pol, on_save, **kwargs): super().__init__(**kwargs) self.pol = pol self.on_save_cb = on_save self.size_hint = (None, None) self.size = (dp(500), dp(600)) self.auto_dismiss = False root = GridLayout( cols=2, spacing=dp(6), padding=dp(10), size_hint=(1, 1) ) # ---------------- NÁZEV ---------------- root.add_widget(Label(text="Display name")) self.d_name = TextInput(text=pol.d_name or "") root.add_widget(self.d_name) root.add_widget(Label(text="Účet name")) self.ch_name = TextInput(text=pol.ch_name or "") root.add_widget(self.ch_name) # ---------------- CENA ---------------- cena = pol.ceny[0] if pol.ceny else None root.add_widget(Label(text="Cena")) self.cena = TextInput(text=str(cena.cena if cena else "0")) root.add_widget(self.cena) root.add_widget(Label(text="Měna")) self.mena = TextInput(text=cena.mena if cena else "CZK") root.add_widget(self.mena) root.add_widget(Label(text="DPH")) self.dan = TextInput(text=cena.dan if cena else "21") root.add_widget(self.dan) root.add_widget(Label(text="Cenová hladina")) self.price_name = TextInput(text=cena.name if cena else "standard") root.add_widget(self.price_name) # ---------------- FRAKCE ---------------- root.add_widget(Label(text="1/2")) self.cena2 = TextInput(text=str(cena.cena2 if cena else "0")) root.add_widget(self.cena2) root.add_widget(Label(text="1/3")) self.cena3 = TextInput(text=str(cena.cena3 if cena else "0")) root.add_widget(self.cena3) root.add_widget(Label(text="1/4")) self.cena4 = TextInput(text=str(cena.cena4 if cena else "0")) root.add_widget(self.cena4) # ---------------- BUTTONY ---------------- btn_save = Button(text="Uložit") btn_cancel = Button(text="Zrušit") btn_save.bind(on_press=self._save) btn_cancel.bind(on_press=lambda *_: self.dismiss()) root.add_widget(btn_save) root.add_widget(btn_cancel) self.add_widget(root) def _save(self, *_): try: cena = Cena( cena=float(self.cena.text), mena=self.mena.text, dan=self.dan.text, name=self.price_name.text, cena2=float(self.cena2.text), cena3=float(self.cena3.text), cena4=float(self.cena4.text), ) self.pol.d_name = self.d_name.text self.pol.ch_name = self.ch_name.text self.pol.ceny = [cena] if self.on_save_cb: self.on_save_cb(self.pol) self.dismiss() except Exception as e: print("chyba při ukládání:", e) class CenikButton(Button): def __init__(self, pol=None, on_select=None, on_long_press=None, **kwargs): price_updater = kwargs.pop("price_updater", None) super().__init__(**kwargs) self.pol = pol self.on_select_cb = on_select self.on_long_press_cb = on_long_press self.price_updater = price_updater self._lp_event = None self._long_pressed = False Clock.schedule_once(lambda *_: self.refresh(), 0) def refresh(self): if self.pol and self.price_updater: self.price_updater(self) # 🔥 KLÍČOVÁ OPRAVA def on_touch_down(self, touch): if not self.collide_point(*touch.pos): return False self._long_pressed = False self._lp_event = Clock.schedule_once(self._trigger_long_press, 0.5) return True # ❗ NEVOLAT super() def on_touch_up(self, touch): if not self.collide_point(*touch.pos): return False if self._lp_event: self._lp_event.cancel() self._lp_event = None if not self._long_pressed: if self.on_select_cb and self.pol: self.on_select_cb(self.pol) return True # ❗ NEVOLAT super() def _trigger_long_press(self, *_): self._long_pressed = True if self.on_long_press_cb and self.pol: self.on_long_press_cb(self.pol) class CenikEditor(Screen): def __init__(self, cenik=None, menu_cols=10, menu_rows=8, **kwargs): cenik = kwargs.pop("cenik", cenik) super().__init__(**kwargs) # ================= DATA ================= self.cenik = cenik self.cols = menu_cols self.rows = menu_rows # ================= STAV ================= self.current_page = 1 levels = self.cenik.used_price_levels() if self.cenik else [] self.price_level = levels[0] if levels else "standard" self._selected_pol = None self._move_mode = False self.cell_w = dp(80) self.cell_h = dp(80) # ================= ROOT ================= root = BoxLayout( orientation="vertical", spacing=dp(6), padding=dp(6), size_hint=(1, 1), ) # ================= SCROLL + GRID ================= self.scroll = ScrollView( do_scroll_x=True, do_scroll_y=True, size_hint=(1, 1), bar_width=dp(6), effect_cls=ScrollEffect, ) self.grid = FloatLayout(size_hint=(None, None)) self.grid.size = ( self.cols * self.cell_w, self.rows * self.cell_h ) self.scroll.add_widget(self.grid) root.add_widget(self.scroll) # ================= SPODNÍ PANEL ================= bottom = BoxLayout( size_hint=(1, None), height=dp(90), spacing=dp(6), padding=dp(6) ) # --- tlačítka --- self.btn_new = Button(text="Nový\nbutton") self.btn_page = Button(text="Nová\nstránka") self.btn_switch = Button(text=f"Stránka\n{self.current_page}") self.btn_save = Button(text="Uložit") self.btn_delete = Button(text="Smazat") self.btn_move = Button(text="Pozice") btn_levels = Button(text="Cenové\nhladiny") btn_price = Button(text=f"Cena\n{self.price_level}") # --- bindy --- self.btn_new.bind(on_press=self._new_button) self.btn_page.bind(on_press=self._new_page) self.btn_switch.bind(on_press=self._switch_page) self.btn_save.bind(on_press=self._save) self.btn_delete.bind(on_press=self._delete_item) self.btn_move.bind(on_press=self._start_move) btn_levels.bind(on_press=self._edit_price_levels) btn_price.bind(on_press=self.on_select_price_level) # --- ulož reference --- self.btn_price = btn_price self._btn_move_color = self.btn_move.background_color # --- add --- for w in [ self.btn_new, self.btn_page, self.btn_switch, self.btn_save, self.btn_delete, self.btn_move, btn_levels, btn_price, ]: bottom.add_widget(w) root.add_widget(bottom) # ================= ADD ROOT ================= self.add_widget(root) # ================= INIT BUILD ================= Clock.schedule_once(lambda *_: self._build(), 0) # scroll nahoru (jako POS) Clock.schedule_once(lambda *_: setattr(self.scroll, "scroll_y", 1), 0) def _open_item_editor(self, pol): modal = ModalView(size_hint=(0.6, 0.9)) root = BoxLayout( orientation="vertical", spacing=dp(6), padding=dp(10), ) # ================= NÁZEV ================= ti_name = TextInput( text=pol.d_name, multiline=False, size_hint=(1, None), height=dp(50) ) root.add_widget(ti_name) # ================= BARVA ================= btn_color = Button( text=f"Barva: {pol.color}", size_hint=(1, None), height=dp(50) ) def change_color(*_): pol.color = (pol.color + 1) % 6 btn_color.text = f"Barva: {pol.color}" btn_color.bind(on_press=change_color) root.add_widget(btn_color) # ================= ŠÍŘKA ================= pos = next((p for p in pol.pos_pc if p.page == self.current_page), None) ti_width = TextInput( text=str(pos.sirka if pos else 1), multiline=False, size_hint=(1, None), height=dp(50) ) root.add_widget(ti_width) # ================= CENY ================= levels = self.cenik.used_price_levels() ceny_map = {c.name: c for c in pol.ceny} grid = GridLayout( cols=2, size_hint=(1, None), spacing=dp(4) ) grid.bind(minimum_height=grid.setter("height")) inputs = {} for lvl in levels: grid.add_widget(Button(text=lvl, size_hint=(1, None), height=dp(40))) cena = ceny_map.get(lvl) ti = TextInput( text=str(cena.cena if cena else ""), multiline=False, size_hint=(1, None), height=dp(40), background_color=(0.2, 0.7, 0.2, 1) if cena else (0.5, 0.5, 0.5, 1) ) inputs[lvl] = ti grid.add_widget(ti) root.add_widget(grid) # ================= ULOŽIT ================= def save(*_): # název pol.d_name = ti_name.text # šířka try: w = max(1, int(ti_width.text)) except: w = 1 for p in pol.pos_pc: if p.page == self.current_page: p.sirka = w # ceny new_ceny = [] for lvl, ti in inputs.items(): txt = ti.text.strip() if not txt: continue try: val = float(txt) except: val = 0 new_ceny.append(Cena( name=lvl, cena=val, mena="Kc", dan="21", cena2=0, cena3=0, cena4=0 )) pol.ceny = new_ceny modal.dismiss() self._build() btn_save = Button(text="Uložit", size_hint=(1, None), height=dp(60)) btn_save.bind(on_press=save) root.add_widget(btn_save) modal.add_widget(root) modal.open() def _edit_price(self, pol, level): root = BoxLayout(orientation="vertical", spacing=dp(6), padding=dp(10)) # najdi cenu cena = next((c for c in pol.ceny if c.name == level), None) ti = TextInput( text=str(cena.cena if cena else 0), multiline=False ) root.add_widget(ti) def save(*_): try: val = float(ti.text) except: val = 0 if cena: cena.cena = val else: pol.ceny.append(Cena( name=level, cena=val, mena="Kc", dan="21", cena2=0, cena3=0, cena4=0 )) modal.dismiss() self._build() btn_save = Button(text="Uložit") btn_save.bind(on_press=save) root.add_widget(btn_save) modal = ModalView(size_hint=(0.4, 0.3)) modal.add_widget(root) modal.open() """ def _open_price_menu(self, pol): levels = self.cenik.used_price_levels() root = BoxLayout( orientation="vertical", spacing=dp(6), padding=dp(10), ) # ===== HLAVIČKA ===== root.add_widget(Button( text=pol.d_name, size_hint=(1, None), height=dp(50), disabled=True )) # ===== MAPA EXISTUJÍCÍCH CEN ===== existing = {c.name: c for c in pol.ceny} # ===== TLAČÍTKA HLADIN ===== for lvl in levels: cena = existing.get(lvl) has_price = cena and cena.cena > 0 btn = Button( text=lvl, size_hint=(1, None), height=dp(50), background_color=(0.2, 0.7, 0.2, 1) if has_price else (0.5, 0.5, 0.5, 1) ) btn.bind(on_press=lambda b, lvl=lvl: self._edit_price(pol, lvl)) root.add_widget(btn) modal = ModalView(size_hint=(0.5, 0.7)) modal.add_widget(root) modal.open() """ def on_select_price_level(self, *_): levels = self.cenik.used_price_levels() if not levels: return try: idx = levels.index(self.price_level) except ValueError: idx = 0 self.price_level = levels[(idx + 1) % len(levels)] print("price_level:", self.price_level) # update tlačítka if hasattr(self, "btn_price"): self.btn_price.text = f"Cena\n{self.price_level}" self._build() def _select_price_level(self, *_): levels = self.cenik.used_price_levels() if not levels: return # jednoduchý cyklus idx = levels.index(self.price_level) self.price_level = levels[(idx + 1) % len(levels)] print("price_level:", self.price_level) self._build() def _edit_price_levels(self, *_): print("OPEN LEVEL EDITOR") # 👈 debug levels = list(self.cenik.used_price_levels()) root = BoxLayout( orientation="vertical", spacing=dp(6), padding=dp(10), ) inputs = [] for lvl in levels: ti = TextInput(text=lvl, multiline=False) inputs.append(ti) root.add_widget(ti) def add_level(*_): ti = TextInput(text="", multiline=False) inputs.append(ti) root.add_widget(ti) btn_add = Button(text="Přidat") btn_add.bind(on_press=add_level) root.add_widget(btn_add) def save(*_): new_levels = [ti.text.strip() for ti in inputs if ti.text.strip()] if not new_levels: print("prázdné hladiny – ignoruji") return self._apply_price_levels(new_levels) modal.dismiss() btn_save = Button(text="Uložit") btn_save.bind(on_press=save) root.add_widget(btn_save) modal = ModalView(size_hint=(0.6, 0.8)) modal.add_widget(root) modal.open() def _apply_price_levels(self, new_levels): for pol in self.cenik.cenpol: existing = {c.name: c for c in pol.ceny} new_ceny = [] for lvl in new_levels: if lvl in existing: new_ceny.append(existing[lvl]) else: # nová hladina → default cena new_ceny.append( Cena( name=lvl, cena=0.0, mena="Kc", dan="21", cena2=0.0, cena3=0.0, cena4=0.0, ) ) pol.ceny = new_ceny # reset aktuální hladiny pokud zmizela if self.price_level not in new_levels: self.price_level = new_levels[0] print("nové hladiny:", new_levels) self._build() # ----------------------------------------------------- def _move_selected_to(self, col, row): if not self._selected_pol: return old_pos = None for p in self._selected_pol.pos_pc: if p.page == self.current_page: old_pos = p break if old_pos is None: sirka = 3 color = 0 else: sirka = max(1, getattr(old_pos, "sirka", 1)) color = getattr(old_pos, "color", 0) # ochrana proti přetečení doprava if col + sirka > self.cols: col = max(0, self.cols - sirka) new_pos = Position( page=self.current_page, col=col, line=row, sirka=sirka, color=color, ) replaced = False new_list = [] for p in self._selected_pol.pos_pc: if p.page == self.current_page and not replaced: new_list.append(new_pos) replaced = True else: new_list.append(p) if not replaced: new_list.append(new_pos) self._selected_pol.pos_pc = new_list self._build() def _select_item(self, pol): self._selected_pol = pol self._build() def _find_free(self, sirka): occupied = set() # obsazené buňky (rozšířené o šířku) for pol in self.cenik.cenpol: for pos in pol.positions: if pos.page != self.current_page: continue w = max(1, getattr(pos, "sirka", 1)) for dx in range(w): occupied.add((pos.col + dx, pos.line)) # hledání místa for row in range(self.rows): for col in range(self.cols): # nevejde se do gridu? if col + sirka > self.cols: continue # kontrola kolize collision = False for dx in range(sirka): if (col + dx, row) in occupied: collision = True break if not collision: return col, row return None def _new_button(self, *_): dlg = WidthDialog( on_ok=self._create_button_with_width, default=3 ) dlg.open() def _create_button_with_width(self, sirka): free = self._find_free(sirka) if not free: print("Není místo pro button této šířky") return col, row = free self._create_item(col, row, sirka) def _new_page(self, *_): max_page = 1 for pol in self.cenik.cenpol: for pos in pol.positions: max_page = max(max_page, pos.page) self.current_page = max_page + 1 print("nová stránka:", self.current_page) self._build() def _switch_page(self, *_): max_page = 1 for pol in self.cenik.cenpol: for pos in pol.positions: max_page = max(max_page, pos.page) self.current_page += 1 if self.current_page > max_page: self.current_page = 1 print("stránka:", self.current_page) self._build() def _save(self, *_): print("UKLÁDÁM") self.cenik.prn() def _pos_to_xy(self, col, row): x = col * self.cell_w y = self.grid.height - (row + 1) * self.cell_h return x, y # ----------------------------------------------------- def _build(self): self.grid.clear_widgets() for pol in self.cenik.cenpol: for pos in pol.positions: if pos.page != self.current_page: continue x, y = self._pos_to_xy(pos.col, pos.line) width_cells = max(1, getattr(pos, "sirka", 1)) btn = CenikButton( pol=pol, on_select=self._select_item, on_long_press=self._open_item_editor, price_updater=self._update_button_price, text=pol.d_name, size=(self.cell_w * width_cells, self.cell_h), size_hint=(None, None), pos=(x, y), ) # zvýraznění výběru if pol == self._selected_pol: btn.background_color = (1, 0.5, 0.3, 1) else: btn.background_color = (1, 1, 1, 1) self.grid.add_widget(btn) def _update_button_price(self, btn): pol = getattr(btn, "pol", None) if not pol: btn.text = "" return # najdi cenu podle hladiny cena = None for c in pol.ceny: if c.name == self.price_level: cena = c break if not cena and pol.ceny: cena = pol.ceny[0] if not cena: btn.text = pol.d_name return # formát jako v POS btn.text = f"{pol.d_name}\n{cena.cena:.2f} {cena.mena}" # ----------------------------------------------------- def _find_item(self, col, row): for pol in self.cenik.cenpol: for pos in pol.positions: if ( pos.page == self.current_page and pos.col == col and pos.line == row ): return pol return None # ----------------------------------------------------- def _create_item(self, col, row, sirka=3): import time # ořez na grid if col + sirka > self.cols: sirka = self.cols - col # fallback kdyby něco selhalo if sirka < 1: sirka = 1 new_id = int(time.time() * 1000) pol = CenPolCreate( id_card=new_id, kod=new_id, d_name=f"Item {len(self.cenik.cenpol) + 1}", ch_name=f"Item {len(self.cenik.cenpol) + 1}", pokl="A", sklad="A", ceny=[ Cena( cena=100, mena="CZK", dan="21", name="standard", cena2=50, cena3=33, cena4=25, ) ], pos_pc=[ Position( page=self.current_page, col=col, line=row, sirka=sirka, ) ], ) # Převedení na CenPol (DB model) self.cenik.cenpol.append(CenPol(**pol.model_dump())) print(f"Přidán button: {pol.d_name} @ ({col},{row}) w={sirka}") self._build() # ----------------------------------------------------- """ def _edit_item(self, pol): self._selected_pol = pol dlg = CenikItemEditor( pol=pol, on_save=self._on_item_saved ) dlg.open() self._build() """ def _on_item_saved(self, pol): print("Uložen:", pol.d_name) self._build() def _delete_item(self, *_): if not self._selected_pol: print("Není vybraná položka") return print("Mažu:", self._selected_pol.d_name) self.cenik.cenpol = [ p for p in self.cenik.cenpol if p != self._selected_pol ] self._selected_pol = None self._build() def _delete_item(self, *_): if not self._selected_pol: print("není vybraná položka") return print("Mažu:", self._selected_pol.d_name) self.cenik.cenpol = [ p for p in self.cenik.cenpol if p != self._selected_pol ] self._selected_pol = None self._build() def _start_move(self, *_): if not self._selected_pol: print("Není vybraná položka") return print("Klikni do gridu pro novou pozici") self._move_mode = True # zvýraznění tlačítka self.btn_move.background_color = (1, 0.4, 0.2, 1) self.btn_move.text = "Klikni\ndo gridu" def _can_place(self, col, row, sirka): # mimo grid if col < 0 or row < 0 or col + sirka > self.cols or row >= self.rows: return False occupied = set() for pol in self.cenik.cenpol: if pol == self._selected_pol: continue # ignorujeme přesouvaný button for pos in pol.positions: if pos.page != self.current_page: continue w = max(1, getattr(pos, "sirka", 1)) for dx in range(w): occupied.add((pos.col + dx, pos.line)) # kontrola kolize for dx in range(sirka): if (col + dx, row) in occupied: return False return True def set_cenik(self, cenik): self.cenik = cenik levels = self.cenik.used_price_levels() self.price_level = levels[0] if levels else "standard" from kivy.clock import Clock Clock.schedule_once(lambda *_: self._build(), 0) def on_touch_down(self, touch): # ================= MOVE REŽIM ================= if self._move_mode: # není vybraná položka if not self._selected_pol: print("žádná vybraná položka") self._move_mode = False return super().on_touch_down(touch) # klik mimo grid = zrušit move if not self.grid.collide_point(*touch.pos): self._move_mode = False self.btn_move.background_color = self._btn_move_color self.btn_move.text = "Pozice" return super().on_touch_down(touch) # ================= ZACHOVÁNÍ ŠÍŘKY ================= old_pos = None for p in self._selected_pol.pos_pc: if p.page == self.current_page: old_pos = p break sirka = max(1, getattr(old_pos, "sirka", 3)) if old_pos else 3 color = getattr(old_pos, "color", 0) if old_pos else 0 # ================= SOUŘADNICE ================= lx = touch.x - self.grid.x ly = touch.y - self.grid.y col = int(lx // self.cell_w) row = int((self.grid.height - ly) // self.cell_h) print(f"move na: {col},{row} (w={sirka})") # ================= OCHRANA GRID ================= if col + sirka > self.cols: col = max(0, self.cols - sirka) if col < 0 or row < 0 or row >= self.rows: print("mimo grid") return True # ================= KOLIZE ================= if not self._can_place(col, row, sirka): print("kolize, přesun zrušen") return True # ================= ULOŽENÍ ================= replaced = False new_list = [] for p in self._selected_pol.pos_pc: if p.page == self.current_page and not replaced: new_list.append(Position( page=self.current_page, col=col, line=row, sirka=sirka, color=color, )) replaced = True else: new_list.append(p) if not replaced: new_list.append(Position( page=self.current_page, col=col, line=row, sirka=sirka, color=color, )) self._selected_pol.pos_pc = new_list # ================= RESET ================= self._move_mode = False self.btn_move.background_color = self._btn_move_color self.btn_move.text = "Pozice" self._build() return True # ================= DEFAULT ================= return super().on_touch_down(touch) # ========================================================= # APP # ========================================================= class CenikEditorApp(App): def build(self): cenik = create_dummy_cenik() return CenikEditor(cenik=cenik) # ========================================================= if __name__ == "__main__": Window.size = (1000, 700) Window.minimum_width = 800 Window.minimum_height = 600 CenikEditorApp().run()